@@ -1,9 +1,10 @@ |
||
1 | 1 |
# Changes |
2 | 2 |
|
3 |
-* 0.3 (Dec 22, 2013) - Agents now have an optional keep_events_for option that is propagated to created events' expires_at field for eventual cleanup. |
|
3 |
+* 0.31 (Jan 2, 2014) - Agents now have an optional keep\_events\_for option that is propagated to created events' expires\_at field, and they update their events' expires\_at fields on change. |
|
4 |
+* 0.3 (Jan 1, 2014) - Remove symbolization of memory, options, and payloads; convert memory, options, and payloads to JSON from YAML. Migration will perform conversion and adjust tables to be UTF-8. Recommend making a DB backup before migrating. |
|
4 | 5 |
* 0.2 (Nov 6, 2013) - PeakDetectorAgent now uses `window_duration_in_days` and `min_peak_spacing_in_days`. Additionally, peaks trigger when the time series rises over the standard deviation multiple, not after it starts to fall. |
5 | 6 |
* June 29, 2013 - Removed rails\_admin because it was causing deployment issues. Better to have people install their favorite admin tool if they want one. |
6 | 7 |
* June, 2013 - A number of new agents have been contributed, including interfaces to Weibo, Twitter, and Twilio, as well as Agents for translation, sentiment analysis, and for posting and receiving webhooks. |
7 |
-* March 24, 2013 (0.1) - Refactored loading of Agents for `check` and `receive` to use ids instead of full objects. This should fix the too-large delayed_job issues. Added `system_timer` and `fastercsv` to the Gemfile for the Ruby 1.8 platform. |
|
8 |
+* March 24, 2013 (0.1) - Refactored loading of Agents for `check` and `receive` to use ids instead of full objects. This should fix the too-large delayed\_job issues. Added `system_timer` and `fastercsv` to the Gemfile for the Ruby 1.8 platform. |
|
8 | 9 |
* March 18, 2013 - Added Wiki page about the [Agent API](https://github.com/cantino/huginn/wiki/Creating-a-new-agent). |
9 | 10 |
* March 17, 2013 - Switched to JSONPath for defining paths through JSON structures. The WebsiteAgent can now scrape and parse JSON. |
@@ -63,9 +63,9 @@ GEM |
||
63 | 63 |
railties (>= 3.2.6, < 5) |
64 | 64 |
warden (~> 1.2.3) |
65 | 65 |
diff-lcs (1.2.4) |
66 |
- dotenv (0.8.0) |
|
67 |
- dotenv-rails (0.8.0) |
|
68 |
- dotenv (= 0.8.0) |
|
66 |
+ dotenv (0.9.0) |
|
67 |
+ dotenv-rails (0.9.0) |
|
68 |
+ dotenv (= 0.9.0) |
|
69 | 69 |
em-http-request (1.0.3) |
70 | 70 |
addressable (>= 2.2.3) |
71 | 71 |
cookiejar |
@@ -13,7 +13,10 @@ Huginn is a system for building agents that perform automated tasks for you onli |
||
13 | 13 |
* Track the weather and get an email when it's going to rain (or snow) tomorrow |
14 | 14 |
* Follow your project names on Twitter and get updates when people mention them |
15 | 15 |
* Scrape websites and receive emails when they change |
16 |
+* Compose digest emails about things you care about to be sent at specific times of the day |
|
17 |
+* Track counts of high frequency events and SMS on changes, such as the term "san francisco emergency" |
|
16 | 18 |
* Track your location over time |
19 |
+* Create Amazon Mechanical Turk tasks as the input, or output, of events |
|
17 | 20 |
|
18 | 21 |
Follow [@tectonic](https://twitter.com/tectonic) for updates as Huginn evolves, and join us in \#huginn on Freenode IRC to discuss the project. |
19 | 22 |
|
@@ -84,7 +87,7 @@ In order to use the WeatherAgent you need an [API key with Wunderground](http:// |
||
84 | 87 |
|
85 | 88 |
You can use [Post Location](https://github.com/cantino/post_location) on your iPhone to post your location to an instance of the UserLocationAgent. Make a new one to see instructions. |
86 | 89 |
|
87 |
-#### Enable DelayedJobWeb for handy delayed_job monitoring and control |
|
90 |
+#### Enable DelayedJobWeb for handy delayed\_job monitoring and control |
|
88 | 91 |
|
89 | 92 |
* Edit `config.ru`, uncomment the DelayedJobWeb section, and change the DelayedJobWeb username and password. |
90 | 93 |
* Uncomment `match "/delayed_job" => DelayedJobWeb, :anchor => false` in `config/routes.rb`. |
@@ -110,6 +113,5 @@ Please fork, add specs, and send pull requests! |
||
110 | 113 |
|
111 | 114 |
[](https://travis-ci.org/cantino/huginn) [](https://codeclimate.com/github/cantino/huginn) |
112 | 115 |
|
113 |
- |
|
114 | 116 |
[](https://bitdeli.com/free "Bitdeli Badge") |
115 | 117 |
|
@@ -1 +1 @@ |
||
1 |
-0.2 |
|
1 |
+0.3 |
@@ -108,6 +108,7 @@ $(document).ready -> |
||
108 | 108 |
$("#logs .refresh, #logs .clear").hide() |
109 | 109 |
$.post "/agents/#{agentId}/logs/clear", { "_method": "DELETE" }, (html) => |
110 | 110 |
$("#logs .logs").html html |
111 |
+ $("#show-tabs li a.recent-errors").removeClass 'recent-errors' |
|
111 | 112 |
$("#logs .spinner").stop(true, true).fadeOut -> |
112 | 113 |
$("#logs .refresh, #logs .clear").show() |
113 | 114 |
|
@@ -122,3 +122,7 @@ span.not-applicable:after { |
||
122 | 122 |
margin: 0 10px; |
123 | 123 |
} |
124 | 124 |
} |
125 |
+ |
|
126 |
+#show-tabs li a.recent-errors { |
|
127 |
+ font-weight: bold; |
|
128 |
+} |
@@ -1,18 +1,18 @@ |
||
1 | 1 |
module EmailConcern |
2 | 2 |
extend ActiveSupport::Concern |
3 | 3 |
|
4 |
- MAIN_KEYS = %w[title message text main value].map(&:to_sym) |
|
4 |
+ MAIN_KEYS = %w[title message text main value] |
|
5 | 5 |
|
6 | 6 |
included do |
7 | 7 |
self.validate :validate_email_options |
8 | 8 |
end |
9 | 9 |
|
10 | 10 |
def validate_email_options |
11 |
- errors.add(:base, "subject and expected_receive_period_in_days are required") unless options[:subject].present? && options[:expected_receive_period_in_days].present? |
|
11 |
+ errors.add(:base, "subject and expected_receive_period_in_days are required") unless options['subject'].present? && options['expected_receive_period_in_days'].present? |
|
12 | 12 |
end |
13 | 13 |
|
14 | 14 |
def working? |
15 |
- last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? |
|
15 |
+ last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
16 | 16 |
end |
17 | 17 |
|
18 | 18 |
def present(payload) |
@@ -7,20 +7,20 @@ module TwitterConcern |
||
7 | 7 |
end |
8 | 8 |
|
9 | 9 |
def validate_twitter_options |
10 |
- unless options[:consumer_key].present? && |
|
11 |
- options[:consumer_secret].present? && |
|
12 |
- options[:oauth_token].present? && |
|
13 |
- options[:oauth_token_secret].present? |
|
10 |
+ unless options['consumer_key'].present? && |
|
11 |
+ options['consumer_secret'].present? && |
|
12 |
+ options['oauth_token'].present? && |
|
13 |
+ options['oauth_token_secret'].present? |
|
14 | 14 |
errors.add(:base, "consumer_key, consumer_secret, oauth_token and oauth_token_secret are required to authenticate with the Twitter API") |
15 | 15 |
end |
16 | 16 |
end |
17 | 17 |
|
18 | 18 |
def configure_twitter |
19 | 19 |
Twitter.configure do |config| |
20 |
- config.consumer_key = options[:consumer_key] |
|
21 |
- config.consumer_secret = options[:consumer_secret] |
|
22 |
- config.oauth_token = options[:oauth_token] || options[:access_key] |
|
23 |
- config.oauth_token_secret = options[:oauth_token_secret] || options[:access_secret] |
|
20 |
+ config.consumer_key = options['consumer_key'] |
|
21 |
+ config.consumer_secret = options['consumer_secret'] |
|
22 |
+ config.oauth_token = options['oauth_token'] || options['access_key'] |
|
23 |
+ config.oauth_token_secret = options['oauth_token_secret'] || options['access_secret'] |
|
24 | 24 |
end |
25 | 25 |
end |
26 | 26 |
|
@@ -6,19 +6,19 @@ module WeiboConcern |
||
6 | 6 |
end |
7 | 7 |
|
8 | 8 |
def validate_weibo_options |
9 |
- unless options[:app_key].present? && |
|
10 |
- options[:app_secret].present? && |
|
11 |
- options[:access_token].present? |
|
9 |
+ unless options['app_key'].present? && |
|
10 |
+ options['app_secret'].present? && |
|
11 |
+ options['access_token'].present? |
|
12 | 12 |
errors.add(:base, "app_key, app_secret and access_token are required") |
13 | 13 |
end |
14 | 14 |
end |
15 | 15 |
|
16 | 16 |
def weibo_client |
17 | 17 |
unless @weibo_client |
18 |
- WeiboOAuth2::Config.api_key = options[:app_key] # WEIBO_APP_KEY |
|
19 |
- WeiboOAuth2::Config.api_secret = options[:app_secret] # WEIBO_APP_SECRET |
|
18 |
+ WeiboOAuth2::Config.api_key = options['app_key'] # WEIBO_APP_KEY |
|
19 |
+ WeiboOAuth2::Config.api_secret = options['app_secret'] # WEIBO_APP_SECRET |
|
20 | 20 |
@weibo_client = WeiboOAuth2::Client.new |
21 |
- @weibo_client.get_token_from_hash :access_token => options[:access_token] |
|
21 |
+ @weibo_client.get_token_from_hash :access_token => options['access_token'] |
|
22 | 22 |
end |
23 | 23 |
@weibo_client |
24 | 24 |
end |
@@ -6,7 +6,7 @@ class EventsController < ApplicationController |
||
6 | 6 |
@agent = current_user.agents.find(params[:agent]) |
7 | 7 |
@events = @agent.events.page(params[:page]) |
8 | 8 |
else |
9 |
- @events = current_user.events.page(params[:page]) |
|
9 |
+ @events = current_user.events.preload(:agent).page(params[:page]) |
|
10 | 10 |
end |
11 | 11 |
|
12 | 12 |
respond_to do |format| |
@@ -7,7 +7,7 @@ class LogsController < ApplicationController |
||
7 | 7 |
end |
8 | 8 |
|
9 | 9 |
def clear |
10 |
- @agent.logs.delete_all |
|
10 |
+ @agent.delete_logs! |
|
11 | 11 |
index |
12 | 12 |
end |
13 | 13 |
|
@@ -1,14 +1,13 @@ |
||
1 |
-require 'serialize_and_symbolize' |
|
1 |
+require 'json_serialized_field' |
|
2 | 2 |
require 'assignable_types' |
3 | 3 |
require 'markdown_class_attributes' |
4 | 4 |
require 'utils' |
5 | 5 |
|
6 | 6 |
class Agent < ActiveRecord::Base |
7 |
- include SerializeAndSymbolize |
|
8 | 7 |
include AssignableTypes |
9 | 8 |
include MarkdownClassAttributes |
9 |
+ include JSONSerializedField |
|
10 | 10 |
|
11 |
- serialize_and_symbolize :options, :memory |
|
12 | 11 |
markdown_class_attributes :description, :event_description |
13 | 12 |
|
14 | 13 |
load_types_in "Agents" |
@@ -20,10 +19,13 @@ class Agent < ActiveRecord::Base |
||
20 | 19 |
|
21 | 20 |
attr_accessible :options, :memory, :name, :type, :schedule, :source_ids, :keep_events_for |
22 | 21 |
|
22 |
+ json_serialize :options, :memory |
|
23 |
+ |
|
23 | 24 |
validates_presence_of :name, :user |
24 | 25 |
validates_inclusion_of :keep_events_for, :in => EVENT_RETENTION_SCHEDULES.map(&:last) |
25 | 26 |
validate :sources_are_owned |
26 | 27 |
validate :validate_schedule |
28 |
+ validate :validate_options |
|
27 | 29 |
|
28 | 30 |
after_initialize :set_default_schedule |
29 | 31 |
before_validation :set_default_schedule |
@@ -36,7 +38,6 @@ class Agent < ActiveRecord::Base |
||
36 | 38 |
has_many :events, :dependent => :delete_all, :inverse_of => :agent, :order => "events.id desc" |
37 | 39 |
has_one :most_recent_event, :inverse_of => :agent, :class_name => "Event", :order => "events.id desc" |
38 | 40 |
has_many :logs, :dependent => :delete_all, :inverse_of => :agent, :class_name => "AgentLog", :order => "agent_logs.id desc" |
39 |
- has_one :most_recent_log, :inverse_of => :agent, :class_name => "AgentLog", :order => "agent_logs.id desc" |
|
40 | 41 |
has_many :received_events, :through => :sources, :class_name => "Event", :source => :events, :order => "events.id desc" |
41 | 42 |
has_many :links_as_source, :dependent => :delete_all, :foreign_key => "source_id", :class_name => "Link", :inverse_of => :source |
42 | 43 |
has_many :links_as_receiver, :dependent => :delete_all, :foreign_key => "receiver_id", :class_name => "Link", :inverse_of => :receiver |
@@ -78,13 +79,16 @@ class Agent < ActiveRecord::Base |
||
78 | 79 |
raise "Implement me in your subclass" |
79 | 80 |
end |
80 | 81 |
|
81 |
- def event_created_within(days) |
|
82 |
- event = most_recent_event |
|
83 |
- event && event.created_at > days.to_i.days.ago && event.payload.present? && event |
|
82 |
+ def validate_options |
|
83 |
+ # Implement me in your subclass to test for valid options. |
|
84 |
+ end |
|
85 |
+ |
|
86 |
+ def event_created_within?(days) |
|
87 |
+ last_event_at && last_event_at > days.to_i.days.ago |
|
84 | 88 |
end |
85 | 89 |
|
86 | 90 |
def recent_error_logs? |
87 |
- most_recent_log.try(:level) == 4 |
|
91 |
+ last_event_at && last_error_log_at && last_error_log_at > (last_event_at - 2.minutes) |
|
88 | 92 |
end |
89 | 93 |
|
90 | 94 |
def create_event(attrs) |
@@ -114,6 +118,14 @@ class Agent < ActiveRecord::Base |
||
114 | 118 |
@memoized_last_event_at ||= most_recent_event.try(:created_at) |
115 | 119 |
end |
116 | 120 |
|
121 |
+ def set_default_schedule |
|
122 |
+ self.schedule = default_schedule unless schedule.present? || cannot_be_scheduled? |
|
123 |
+ end |
|
124 |
+ |
|
125 |
+ def unschedule_if_cannot_schedule |
|
126 |
+ self.schedule = nil if cannot_be_scheduled? |
|
127 |
+ end |
|
128 |
+ |
|
117 | 129 |
def default_schedule |
118 | 130 |
self.class.default_schedule |
119 | 131 |
end |
@@ -181,6 +193,16 @@ class Agent < ActiveRecord::Base |
||
181 | 193 |
update_event_expirations! if keep_events_for_changed? |
182 | 194 |
end |
183 | 195 |
|
196 |
+ def delete_logs! |
|
197 |
+ logs.delete_all |
|
198 |
+ update_column :last_error_log_at, nil |
|
199 |
+ end |
|
200 |
+ |
|
201 |
+ def log(message, options = {}) |
|
202 |
+ puts "Agent##{id}: #{message}" unless Rails.env.test? |
|
203 |
+ AgentLog.log_for_agent(self, message, options) |
|
204 |
+ end |
|
205 |
+ |
|
184 | 206 |
def update_event_expirations! |
185 | 207 |
if keep_events_for == 0 |
186 | 208 |
events.update_all :expires_at => nil |
@@ -14,6 +14,9 @@ class AgentLog < ActiveRecord::Base |
||
14 | 14 |
oldest_id_to_keep = agent.logs.limit(1).offset(log_length - 1).pluck("agent_logs.id") |
15 | 15 |
agent.logs.where("agent_logs.id < ?", oldest_id_to_keep).delete_all |
16 | 16 |
end |
17 |
+ |
|
18 |
+ agent.update_column :last_error_log_at, Time.now if log.level >= 4 |
|
19 |
+ |
|
17 | 20 |
log |
18 | 21 |
end |
19 | 22 |
|
@@ -29,22 +29,22 @@ module Agents |
||
29 | 29 |
|
30 | 30 |
def default_options |
31 | 31 |
{ |
32 |
- :start_date => Date.today.httpdate[0..15], |
|
33 |
- :end_date => Date.today.plus_with_duration(100).httpdate[0..15], |
|
34 |
- :from => "New York", |
|
35 |
- :to => "Chicago", |
|
36 |
- :username => "xx", |
|
37 |
- :password => "xx", |
|
38 |
- :expected_update_period_in_days => "1" |
|
32 |
+ 'start_date' => Date.today.httpdate[0..15], |
|
33 |
+ 'end_date' => Date.today.plus_with_duration(100).httpdate[0..15], |
|
34 |
+ 'from' => "New York", |
|
35 |
+ 'to' => "Chicago", |
|
36 |
+ 'username' => "xx", |
|
37 |
+ 'password' => "xx", |
|
38 |
+ 'expected_update_period_in_days' => "1" |
|
39 | 39 |
} |
40 | 40 |
end |
41 | 41 |
|
42 | 42 |
def working? |
43 |
- event_created_within(options[:expected_update_period_in_days]) && !recent_error_logs? |
|
43 |
+ event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? |
|
44 | 44 |
end |
45 | 45 |
|
46 | 46 |
def validate_options |
47 |
- unless %w[start_date end_date from to username password expected_update_period_in_days].all? { |field| options[field.to_sym].present? } |
|
47 |
+ unless %w[start_date end_date from to username password expected_update_period_in_days].all? { |field| options[field].present? } |
|
48 | 48 |
errors.add(:base, "All fields are required") |
49 | 49 |
end |
50 | 50 |
end |
@@ -54,9 +54,9 @@ module Agents |
||
54 | 54 |
end |
55 | 55 |
|
56 | 56 |
def check |
57 |
- auth_options = {:basic_auth => {:username =>options[:username], :password=>options[:password]}} |
|
58 |
- parse_response = HTTParty.get "http://api.adioso.com/v2/search/parse?q=#{URI.encode(options[:from])}+to+#{URI.encode(options[:to])}", auth_options |
|
59 |
- fare_request = parse_response["search_url"].gsub /(end=)(\d*)([^\d]*)(\d*)/, "\\1#{date_to_unix_epoch(options[:end_date])}\\3#{date_to_unix_epoch(options[:start_date])}" |
|
57 |
+ auth_options = {:basic_auth => {:username =>options[:username], :password=>options['password']}} |
|
58 |
+ parse_response = HTTParty.get "http://api.adioso.com/v2/search/parse?q=#{URI.encode(options['from'])}+to+#{URI.encode(options['to'])}", auth_options |
|
59 |
+ fare_request = parse_response["search_url"].gsub /(end=)(\d*)([^\d]*)(\d*)/, "\\1#{date_to_unix_epoch(options['end_date'])}\\3#{date_to_unix_epoch(options['start_date'])}" |
|
60 | 60 |
fare = HTTParty.get fare_request, auth_options |
61 | 61 |
|
62 | 62 |
if fare["warnings"] |
@@ -64,7 +64,7 @@ module Agents |
||
64 | 64 |
else |
65 | 65 |
event = fare["results"].min {|a,b| a["cost"] <=> b["cost"]} |
66 | 66 |
event["date"] = Time.at(event["date"]).to_date.httpdate[0..15] |
67 |
- event["route"] = "#{options[:from]} to #{options[:to]}" |
|
67 |
+ event["route"] = "#{options['from']} to #{options['to']}" |
|
68 | 68 |
create_event :payload => event |
69 | 69 |
end |
70 | 70 |
end |
@@ -9,7 +9,7 @@ module Agents |
||
9 | 9 |
description <<-MD |
10 | 10 |
The DigestEmailAgent collects any Events sent to it and sends them all via email when run. |
11 | 11 |
The email will be sent to your account's address and will have a `subject` and an optional `headline` before |
12 |
- listing the Events. If the Events' payloads contain a `:message`, that will be highlighted, otherwise everything in |
|
12 |
+ listing the Events. If the Events' payloads contain a `message`, that will be highlighted, otherwise everything in |
|
13 | 13 |
their payloads will be shown. |
14 | 14 |
|
15 | 15 |
Set `expected_receive_period_in_days` to the maximum amount of time that you'd expect to pass between Events being received by this Agent. |
@@ -17,29 +17,29 @@ module Agents |
||
17 | 17 |
|
18 | 18 |
def default_options |
19 | 19 |
{ |
20 |
- :subject => "You have some notifications!", |
|
21 |
- :headline => "Your notifications:", |
|
22 |
- :expected_receive_period_in_days => "2" |
|
20 |
+ 'subject' => "You have some notifications!", |
|
21 |
+ 'headline' => "Your notifications:", |
|
22 |
+ 'expected_receive_period_in_days' => "2" |
|
23 | 23 |
} |
24 | 24 |
end |
25 | 25 |
|
26 | 26 |
def receive(incoming_events) |
27 | 27 |
incoming_events.each do |event| |
28 |
- self.memory[:queue] ||= [] |
|
29 |
- self.memory[:queue] << event.payload |
|
30 |
- self.memory[:events] ||= [] |
|
31 |
- self.memory[:events] << event.id |
|
28 |
+ self.memory['queue'] ||= [] |
|
29 |
+ self.memory['queue'] << event.payload |
|
30 |
+ self.memory['events'] ||= [] |
|
31 |
+ self.memory['events'] << event.id |
|
32 | 32 |
end |
33 | 33 |
end |
34 | 34 |
|
35 | 35 |
def check |
36 |
- if self.memory[:queue] && self.memory[:queue].length > 0 |
|
37 |
- ids = self.memory[:events].join(",") |
|
38 |
- groups = self.memory[:queue].map { |payload| present(payload) } |
|
36 |
+ if self.memory['queue'] && self.memory['queue'].length > 0 |
|
37 |
+ ids = self.memory['events'].join(",") |
|
38 |
+ groups = self.memory['queue'].map { |payload| present(payload) } |
|
39 | 39 |
log "Sending digest mail to #{user.email} with events [#{ids}]" |
40 |
- SystemMailer.delay.send_message(:to => user.email, :subject => options[:subject], :headline => options[:headline], :groups => groups) |
|
41 |
- self.memory[:queue] = [] |
|
42 |
- self.memory[:events] = [] |
|
40 |
+ SystemMailer.delay.send_message(:to => user.email, :subject => options['subject'], :headline => options['headline'], :groups => groups) |
|
41 |
+ self.memory['queue'] = [] |
|
42 |
+ self.memory['events'] = [] |
|
43 | 43 |
end |
44 | 44 |
end |
45 | 45 |
end |
@@ -16,16 +16,16 @@ module Agents |
||
16 | 16 |
|
17 | 17 |
def default_options |
18 | 18 |
{ |
19 |
- :subject => "You have a notification!", |
|
20 |
- :headline => "Your notification:", |
|
21 |
- :expected_receive_period_in_days => "2" |
|
19 |
+ 'subject' => "You have a notification!", |
|
20 |
+ 'headline' => "Your notification:", |
|
21 |
+ 'expected_receive_period_in_days' => "2" |
|
22 | 22 |
} |
23 | 23 |
end |
24 | 24 |
|
25 | 25 |
def receive(incoming_events) |
26 | 26 |
incoming_events.each do |event| |
27 | 27 |
log "Sending digest mail to #{user.email} with event #{event.id}" |
28 |
- SystemMailer.delay.send_message(:to => user.email, :subject => options[:subject], :headline => options[:headline], :groups => [present(event.payload)]) |
|
28 |
+ SystemMailer.delay.send_message(:to => user.email, :subject => options['subject'], :headline => options['headline'], :groups => [present(event.payload)]) |
|
29 | 29 |
end |
30 | 30 |
end |
31 | 31 |
end |
@@ -8,20 +8,20 @@ module Agents |
||
8 | 8 |
For example, here is a possible Event: |
9 | 9 |
|
10 | 10 |
{ |
11 |
- :high => { |
|
12 |
- :celsius => "18", |
|
13 |
- :fahreinheit => "64" |
|
11 |
+ "high": { |
|
12 |
+ "celsius": "18", |
|
13 |
+ "fahreinheit": "64" |
|
14 | 14 |
}, |
15 |
- :conditions => "Rain showers", |
|
16 |
- :data => "This is some data" |
|
15 |
+ "conditions": "Rain showers", |
|
16 |
+ "data": "This is some data" |
|
17 | 17 |
} |
18 | 18 |
|
19 | 19 |
You may want to send this event to another Agent, for example a Twilio Agent, which expects a `message` key. |
20 | 20 |
You can use an Event Formatting Agent's `instructions` setting to do this in the following way: |
21 | 21 |
|
22 |
- instructions: { |
|
23 |
- message: "Today's conditions look like <$.conditions> with a high temperature of <$.high.celsius> degrees Celsius.", |
|
24 |
- subject: "$.data" |
|
22 |
+ "instructions": { |
|
23 |
+ "message": "Today's conditions look like <$.conditions> with a high temperature of <$.high.celsius> degrees Celsius.", |
|
24 |
+ "subject": "$.data" |
|
25 | 25 |
} |
26 | 26 |
|
27 | 27 |
JSONPaths must be between < and > . Make sure that you don't use these symbols anywhere else. |
@@ -29,8 +29,8 @@ module Agents |
||
29 | 29 |
Events generated by this possible Event Formatting Agent will look like: |
30 | 30 |
|
31 | 31 |
{ |
32 |
- :message => "Today's conditions look like Rain showers with a high temperature of 18 degrees Celsius.", |
|
33 |
- :subject => "This is some data" |
|
32 |
+ "message": "Today's conditions look like Rain showers with a high temperature of 18 degrees Celsius.", |
|
33 |
+ "subject": "This is some data" |
|
34 | 34 |
} |
35 | 35 |
|
36 | 36 |
If you want to retain original contents of events and only add new keys, then set `mode` to `merge`, otherwise set it to `clean`. |
@@ -40,25 +40,25 @@ module Agents |
||
40 | 40 |
To CGI escape output (for example when creating a link), prefix with `escape`, like so: |
41 | 41 |
|
42 | 42 |
{ |
43 |
- :message => "A peak was on Twitter in <$.group_by>. Search: https://twitter.com/search?q=<escape $.group_by>" |
|
43 |
+ "message": "A peak was on Twitter in <$.group_by>. Search: https://twitter.com/search?q=<escape $.group_by>" |
|
44 | 44 |
} |
45 | 45 |
MD |
46 | 46 |
|
47 | 47 |
event_description "User defined" |
48 | 48 |
|
49 | 49 |
def validate_options |
50 |
- errors.add(:base, "instructions, mode, skip_agent, and skip_created_at all need to be present.") unless options[:instructions].present? and options[:mode].present? and options[:skip_agent].present? and options[:skip_created_at].present? |
|
50 |
+ errors.add(:base, "instructions, mode, skip_agent, and skip_created_at all need to be present.") unless options['instructions'].present? and options['mode'].present? and options['skip_agent'].present? and options['skip_created_at'].present? |
|
51 | 51 |
end |
52 | 52 |
|
53 | 53 |
def default_options |
54 | 54 |
{ |
55 |
- :instructions => { |
|
56 |
- :message => "You received a text <$.text> from <$.fields.from>", |
|
57 |
- :some_other_field => "Looks like the weather is going to be <$.fields.weather>" |
|
55 |
+ 'instructions' => { |
|
56 |
+ 'message' => "You received a text <$.text> from <$.fields.from>", |
|
57 |
+ 'some_other_field' => "Looks like the weather is going to be <$.fields.weather>" |
|
58 | 58 |
}, |
59 |
- :mode => "clean", |
|
60 |
- :skip_agent => "false", |
|
61 |
- :skip_created_at => "false" |
|
59 |
+ 'mode' => "clean", |
|
60 |
+ 'skip_agent' => "false", |
|
61 |
+ 'skip_created_at' => "false" |
|
62 | 62 |
} |
63 | 63 |
end |
64 | 64 |
|
@@ -68,10 +68,10 @@ module Agents |
||
68 | 68 |
|
69 | 69 |
def receive(incoming_events) |
70 | 70 |
incoming_events.each do |event| |
71 |
- formatted_event = options[:mode].to_s == "merge" ? event.payload : {} |
|
72 |
- options[:instructions].each_pair {|key, value| formatted_event[key] = Utils.interpolate_jsonpaths(value, event.payload) } |
|
73 |
- formatted_event[:agent] = Agent.find(event.agent_id).type.slice!(8..-1) unless options[:skip_agent].to_s == "true" |
|
74 |
- formatted_event[:created_at] = event.created_at unless options[:skip_created_at].to_s == "true" |
|
71 |
+ formatted_event = options['mode'].to_s == "merge" ? event.payload : {} |
|
72 |
+ options['instructions'].each_pair {|key, value| formatted_event[key] = Utils.interpolate_jsonpaths(value, event.payload) } |
|
73 |
+ formatted_event['agent'] = Agent.find(event.agent_id).type.slice!(8..-1) unless options['skip_agent'].to_s == "true" |
|
74 |
+ formatted_event['created_at'] = event.created_at unless options['skip_created_at'].to_s == "true" |
|
75 | 75 |
create_event :payload => formatted_event |
76 | 76 |
end |
77 | 77 |
end |
@@ -9,9 +9,13 @@ module Agents |
||
9 | 9 |
|
10 | 10 |
HITs can be created in response to events, or on a schedule. Set `trigger_on` to either `schedule` or `event`. |
11 | 11 |
|
12 |
+ # Schedule |
|
13 |
+ |
|
12 | 14 |
The schedule of this Agent is how often it should check for completed HITs, __NOT__ how often to submit one. To configure how often a new HIT |
13 | 15 |
should be submitted when in `schedule` mode, set `submission_period` to a number of hours. |
14 | 16 |
|
17 |
+ # Example |
|
18 |
+ |
|
15 | 19 |
If created with an event, all HIT fields can contain interpolated values via [JSONPaths](http://goessner.net/articles/JsonPath/) placed between < and > characters. |
16 | 20 |
For example, if the incoming event was a Twitter event, you could make a HITT to rate its sentiment like this: |
17 | 21 |
|
@@ -58,8 +62,52 @@ module Agents |
||
58 | 62 |
which contain `key` and `text`. For _free\\_text_, the special configuration options are all optional, and are |
59 | 63 |
`default`, `min_length`, and `max_length`. |
60 | 64 |
|
61 |
- If all of the `questions` are of `type` _selection_, you can set `take_majority` to _true_ at the top level to |
|
62 |
- automatically select the majority vote for each question across all `assignments`. If all selections are numeric, an `average_answer` will also be generated. |
|
65 |
+ # Combining answers |
|
66 |
+ |
|
67 |
+ There are a couple of ways to combine HITs that have multiple `assignments`, all of which involve setting `combination_mode` at the top level. |
|
68 |
+ |
|
69 |
+ ## Taking the majority |
|
70 |
+ |
|
71 |
+ Option 1: if all of your `questions` are of `type` _selection_, you can set `combination_mode` to `take_majority`. |
|
72 |
+ This will cause the Agent to automatically select the majority vote for each question across all `assignments` and return it as `majority_answer`. |
|
73 |
+ If all selections are numeric, an `average_answer` will also be generated. |
|
74 |
+ |
|
75 |
+ Option 2: you can have the Agent ask additional human workers to rank the `assignments` and return the most highly ranked answer. |
|
76 |
+ To do this, set `combination_mode` to `poll` and provide a `poll_options` object. Here is an example: |
|
77 |
+ |
|
78 |
+ { |
|
79 |
+ "trigger_on": "schedule", |
|
80 |
+ "submission_period": 12, |
|
81 |
+ "combination_mode": "poll", |
|
82 |
+ "poll_options": { |
|
83 |
+ "title": "Take a poll about some jokes", |
|
84 |
+ "instructions": "Please rank these jokes from most funny (5) to least funny (1)", |
|
85 |
+ "assignments": 3, |
|
86 |
+ "row_template": "<$.joke>" |
|
87 |
+ }, |
|
88 |
+ "hit": { |
|
89 |
+ "assignments": 5, |
|
90 |
+ "title": "Tell a joke", |
|
91 |
+ "description": "Please tell me a joke", |
|
92 |
+ "reward": 0.05, |
|
93 |
+ "lifetime_in_seconds": "3600", |
|
94 |
+ "questions": [ |
|
95 |
+ { |
|
96 |
+ "type": "free_text", |
|
97 |
+ "key": "joke", |
|
98 |
+ "name": "Your joke", |
|
99 |
+ "required": "true", |
|
100 |
+ "question": "Joke", |
|
101 |
+ "min_length": "2", |
|
102 |
+ "max_length": "2000" |
|
103 |
+ } |
|
104 |
+ ] |
|
105 |
+ } |
|
106 |
+ } |
|
107 |
+ |
|
108 |
+ Resulting events will have the original `answers`, as well as the `poll` results, and a field called `best_answer` that contains the best answer as determined by the poll. |
|
109 |
+ |
|
110 |
+ # Other settings |
|
63 | 111 |
|
64 | 112 |
`lifetime_in_seconds` is the number of seconds a HIT is left on Amazon before it's automatically closed. The default is 1 day. |
65 | 113 |
|
@@ -70,73 +118,83 @@ module Agents |
||
70 | 118 |
Events look like: |
71 | 119 |
|
72 | 120 |
{ |
121 |
+ "answers": [ |
|
122 |
+ { |
|
123 |
+ "feedback": "Hello!", |
|
124 |
+ "sentiment": "happy" |
|
125 |
+ } |
|
126 |
+ ] |
|
73 | 127 |
} |
74 | 128 |
MD |
75 | 129 |
|
76 | 130 |
def validate_options |
77 |
- options[:hit] ||= {} |
|
78 |
- options[:hit][:questions] ||= [] |
|
79 |
- |
|
80 |
- errors.add(:base, "'trigger_on' must be one of 'schedule' or 'event'") unless %w[schedule event].include?(options[:trigger_on]) |
|
81 |
- errors.add(:base, "'hit.assignments' should specify the number of HIT assignments to create") unless options[:hit][:assignments].present? && options[:hit][:assignments].to_i > 0 |
|
82 |
- errors.add(:base, "'hit.title' must be provided") unless options[:hit][:title].present? |
|
83 |
- errors.add(:base, "'hit.description' must be provided") unless options[:hit][:description].present? |
|
84 |
- errors.add(:base, "'hit.questions' must be provided") unless options[:hit][:questions].present? && options[:hit][:questions].length > 0 |
|
85 |
- |
|
86 |
- if options[:trigger_on] == "event" |
|
87 |
- errors.add(:base, "'expected_receive_period_in_days' is required when 'trigger_on' is set to 'event'") unless options[:expected_receive_period_in_days].present? |
|
88 |
- elsif options[:trigger_on] == "schedule" |
|
89 |
- errors.add(:base, "'submission_period' must be set to a positive number of hours when 'trigger_on' is set to 'schedule'") unless options[:submission_period].present? && options[:submission_period].to_i > 0 |
|
131 |
+ options['hit'] ||= {} |
|
132 |
+ options['hit']['questions'] ||= [] |
|
133 |
+ |
|
134 |
+ errors.add(:base, "'trigger_on' must be one of 'schedule' or 'event'") unless %w[schedule event].include?(options['trigger_on']) |
|
135 |
+ errors.add(:base, "'hit.assignments' should specify the number of HIT assignments to create") unless options['hit']['assignments'].present? && options['hit']['assignments'].to_i > 0 |
|
136 |
+ errors.add(:base, "'hit.title' must be provided") unless options['hit']['title'].present? |
|
137 |
+ errors.add(:base, "'hit.description' must be provided") unless options['hit']['description'].present? |
|
138 |
+ errors.add(:base, "'hit.questions' must be provided") unless options['hit']['questions'].present? && options['hit']['questions'].length > 0 |
|
139 |
+ |
|
140 |
+ if options['trigger_on'] == "event" |
|
141 |
+ errors.add(:base, "'expected_receive_period_in_days' is required when 'trigger_on' is set to 'event'") unless options['expected_receive_period_in_days'].present? |
|
142 |
+ elsif options['trigger_on'] == "schedule" |
|
143 |
+ errors.add(:base, "'submission_period' must be set to a positive number of hours when 'trigger_on' is set to 'schedule'") unless options['submission_period'].present? && options['submission_period'].to_i > 0 |
|
90 | 144 |
end |
91 | 145 |
|
92 |
- if options[:hit][:questions].any? { |question| [:key, :name, :required, :type, :question].any? {|k| !question[k].present? } } |
|
146 |
+ if options['hit']['questions'].any? { |question| %w[key name required type question].any? {|k| !question[k].present? } } |
|
93 | 147 |
errors.add(:base, "all questions must set 'key', 'name', 'required', 'type', and 'question'") |
94 | 148 |
end |
95 | 149 |
|
96 |
- if options[:hit][:questions].any? { |question| question[:type] == "selection" && (!question[:selections].present? || question[:selections].length == 0 || !question[:selections].all? {|s| s[:key].present? } || !question[:selections].all? { |s| s[:text].present? })} |
|
150 |
+ if options['hit']['questions'].any? { |question| question['type'] == "selection" && (!question['selections'].present? || question['selections'].length == 0 || !question['selections'].all? {|s| s['key'].present? } || !question['selections'].all? { |s| s['text'].present? })} |
|
97 | 151 |
errors.add(:base, "all questions of type 'selection' must have a selections array with selections that set 'key' and 'name'") |
98 | 152 |
end |
99 | 153 |
|
100 |
- if options[:take_majority] == "true" && options[:hit][:questions].any? { |question| question[:type] != "selection" } |
|
154 |
+ if take_majority? && options['hit']['questions'].any? { |question| question['type'] != "selection" } |
|
101 | 155 |
errors.add(:base, "all questions must be of type 'selection' to use the 'take_majority' option") |
102 | 156 |
end |
157 |
+ |
|
158 |
+ if create_poll? |
|
159 |
+ errors.add(:base, "poll_options is required when combination_mode is set to 'poll' and must have the keys 'title', 'instructions', 'row_template', and 'assignments'") unless options['poll_options'].is_a?(Hash) && options['poll_options']['title'].present? && options['poll_options']['instructions'].present? && options['poll_options']['row_template'].present? && options['poll_options']['assignments'].to_i > 0 |
|
160 |
+ end |
|
103 | 161 |
end |
104 | 162 |
|
105 | 163 |
def default_options |
106 | 164 |
{ |
107 |
- :expected_receive_period_in_days => 2, |
|
108 |
- :trigger_on => "event", |
|
109 |
- :hit => |
|
165 |
+ 'expected_receive_period_in_days' => 2, |
|
166 |
+ 'trigger_on' => "event", |
|
167 |
+ 'hit' => |
|
110 | 168 |
{ |
111 |
- :assignments => 1, |
|
112 |
- :title => "Sentiment evaluation", |
|
113 |
- :description => "Please rate the sentiment of this message: '<$.message>'", |
|
114 |
- :reward => 0.05, |
|
115 |
- :lifetime_in_seconds => 24 * 60 * 60, |
|
116 |
- :questions => |
|
169 |
+ 'assignments' => 1, |
|
170 |
+ 'title' => "Sentiment evaluation", |
|
171 |
+ 'description' => "Please rate the sentiment of this message: '<$.message>'", |
|
172 |
+ 'reward' => 0.05, |
|
173 |
+ 'lifetime_in_seconds' => 24 * 60 * 60, |
|
174 |
+ 'questions' => |
|
117 | 175 |
[ |
118 | 176 |
{ |
119 |
- :type => "selection", |
|
120 |
- :key => "sentiment", |
|
121 |
- :name => "Sentiment", |
|
122 |
- :required => "true", |
|
123 |
- :question => "Please select the best sentiment value:", |
|
124 |
- :selections => |
|
177 |
+ 'type' => "selection", |
|
178 |
+ 'key' => "sentiment", |
|
179 |
+ 'name' => "Sentiment", |
|
180 |
+ 'required' => "true", |
|
181 |
+ 'question' => "Please select the best sentiment value:", |
|
182 |
+ 'selections' => |
|
125 | 183 |
[ |
126 |
- { :key => "happy", :text => "Happy" }, |
|
127 |
- { :key => "sad", :text => "Sad" }, |
|
128 |
- { :key => "neutral", :text => "Neutral" } |
|
184 |
+ { 'key' => "happy", 'text' => "Happy" }, |
|
185 |
+ { 'key' => "sad", 'text' => "Sad" }, |
|
186 |
+ { 'key' => "neutral", 'text' => "Neutral" } |
|
129 | 187 |
] |
130 | 188 |
}, |
131 | 189 |
{ |
132 |
- :type => "free_text", |
|
133 |
- :key => "feedback", |
|
134 |
- :name => "Have any feedback for us?", |
|
135 |
- :required => "false", |
|
136 |
- :question => "Feedback", |
|
137 |
- :default => "Type here...", |
|
138 |
- :min_length => "2", |
|
139 |
- :max_length => "2000" |
|
190 |
+ 'type' => "free_text", |
|
191 |
+ 'key' => "feedback", |
|
192 |
+ 'name' => "Have any feedback for us?", |
|
193 |
+ 'required' => "false", |
|
194 |
+ 'question' => "Feedback", |
|
195 |
+ 'default' => "Type here...", |
|
196 |
+ 'min_length' => "2", |
|
197 |
+ 'max_length' => "2000" |
|
140 | 198 |
} |
141 | 199 |
] |
142 | 200 |
} |
@@ -144,110 +202,205 @@ module Agents |
||
144 | 202 |
end |
145 | 203 |
|
146 | 204 |
def working? |
147 |
- last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? |
|
205 |
+ last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
148 | 206 |
end |
149 | 207 |
|
150 | 208 |
def check |
151 | 209 |
review_hits |
152 | 210 |
|
153 |
- if options[:trigger_on] == "schedule" && (memory[:last_schedule] || 0) <= Time.now.to_i - options[:submission_period].to_i * 60 * 60 |
|
154 |
- memory[:last_schedule] = Time.now.to_i |
|
155 |
- create_hit |
|
211 |
+ if options['trigger_on'] == "schedule" && (memory['last_schedule'] || 0) <= Time.now.to_i - options['submission_period'].to_i * 60 * 60 |
|
212 |
+ memory['last_schedule'] = Time.now.to_i |
|
213 |
+ create_basic_hit |
|
156 | 214 |
end |
157 | 215 |
end |
158 | 216 |
|
159 | 217 |
def receive(incoming_events) |
160 |
- if options[:trigger_on] == "event" |
|
218 |
+ if options['trigger_on'] == "event" |
|
161 | 219 |
incoming_events.each do |event| |
162 |
- create_hit event |
|
220 |
+ create_basic_hit event |
|
163 | 221 |
end |
164 | 222 |
end |
165 | 223 |
end |
166 | 224 |
|
167 | 225 |
protected |
168 | 226 |
|
227 |
+ def take_majority? |
|
228 |
+ options['combination_mode'] == "take_majority" || options['take_majority'] == "true" |
|
229 |
+ end |
|
230 |
+ |
|
231 |
+ def create_poll? |
|
232 |
+ options['combination_mode'] == "poll" |
|
233 |
+ end |
|
234 |
+ |
|
235 |
+ def event_for_hit(hit_id) |
|
236 |
+ if memory['hits'][hit_id].is_a?(Hash) |
|
237 |
+ Event.find_by_id(memory['hits'][hit_id]['event_id']) |
|
238 |
+ else |
|
239 |
+ nil |
|
240 |
+ end |
|
241 |
+ end |
|
242 |
+ |
|
243 |
+ def hit_type(hit_id) |
|
244 |
+ if memory['hits'][hit_id].is_a?(Hash) && memory['hits'][hit_id]['type'] |
|
245 |
+ memory['hits'][hit_id]['type'] |
|
246 |
+ else |
|
247 |
+ 'user' |
|
248 |
+ end |
|
249 |
+ end |
|
250 |
+ |
|
169 | 251 |
def review_hits |
170 | 252 |
reviewable_hit_ids = RTurk::GetReviewableHITs.create.hit_ids |
171 |
- my_reviewed_hit_ids = reviewable_hit_ids & (memory[:hits] || {}).keys.map(&:to_s) |
|
253 |
+ my_reviewed_hit_ids = reviewable_hit_ids & (memory['hits'] || {}).keys |
|
172 | 254 |
if reviewable_hit_ids.length > 0 |
173 | 255 |
log "MTurk reports #{reviewable_hit_ids.length} HITs, of which I own [#{my_reviewed_hit_ids.to_sentence}]" |
174 | 256 |
end |
257 |
+ |
|
175 | 258 |
my_reviewed_hit_ids.each do |hit_id| |
176 | 259 |
hit = RTurk::Hit.new(hit_id) |
177 | 260 |
assignments = hit.assignments |
178 | 261 |
|
179 | 262 |
log "Looking at HIT #{hit_id}. I found #{assignments.length} assignments#{" with the statuses: #{assignments.map(&:status).to_sentence}" if assignments.length > 0}" |
180 | 263 |
if assignments.length == hit.max_assignments && assignments.all? { |assignment| assignment.status == "Submitted" } |
181 |
- payload = { :answers => assignments.map(&:answers) } |
|
182 |
- |
|
183 |
- if options[:take_majority] == "true" |
|
184 |
- counts = {} |
|
185 |
- options[:hit][:questions].each do |question| |
|
186 |
- question_counts = question[:selections].inject({}) { |memo, selection| memo[selection[:key]] = 0; memo } |
|
187 |
- assignments.each do |assignment| |
|
188 |
- answers = ActiveSupport::HashWithIndifferentAccess.new(assignment.answers) |
|
189 |
- answer = answers[question[:key]] |
|
190 |
- question_counts[answer] += 1 |
|
264 |
+ inbound_event = event_for_hit(hit_id) |
|
265 |
+ |
|
266 |
+ if hit_type(hit_id) == 'poll' |
|
267 |
+ # handle completed polls |
|
268 |
+ |
|
269 |
+ log "Handling a poll: #{hit_id}" |
|
270 |
+ |
|
271 |
+ scores = {} |
|
272 |
+ assignments.each do |assignment| |
|
273 |
+ assignment.answers.each do |index, rating| |
|
274 |
+ scores[index] ||= 0 |
|
275 |
+ scores[index] += rating.to_i |
|
191 | 276 |
end |
192 |
- counts[question[:key]] = question_counts |
|
193 | 277 |
end |
194 |
- payload[:counts] = counts |
|
195 | 278 |
|
196 |
- majority_answer = counts.inject({}) do |memo, (key, question_counts)| |
|
197 |
- memo[key] = question_counts.to_a.sort {|a, b| a.last <=> b.last }.last.first |
|
198 |
- memo |
|
199 |
- end |
|
200 |
- payload[:majority_answer] = majority_answer |
|
201 |
- |
|
202 |
- if all_questions_are_numeric? |
|
203 |
- average_answer = counts.inject({}) do |memo, (key, question_counts)| |
|
204 |
- sum = divisor = 0 |
|
205 |
- question_counts.to_a.each do |num, count| |
|
206 |
- sum += num.to_s.to_f * count |
|
207 |
- divisor += count |
|
279 |
+ top_answer = scores.to_a.sort {|b, a| a.last <=> b.last }.first.first |
|
280 |
+ |
|
281 |
+ payload = { |
|
282 |
+ 'answers' => memory['hits'][hit_id]['answers'], |
|
283 |
+ 'poll' => assignments.map(&:answers), |
|
284 |
+ 'best_answer' => memory['hits'][hit_id]['answers'][top_answer.to_i - 1] |
|
285 |
+ } |
|
286 |
+ |
|
287 |
+ event = create_event :payload => payload |
|
288 |
+ log "Event emitted with answer(s) for poll", :outbound_event => event, :inbound_event => inbound_event |
|
289 |
+ else |
|
290 |
+ # handle normal completed HITs |
|
291 |
+ payload = { 'answers' => assignments.map(&:answers) } |
|
292 |
+ |
|
293 |
+ if take_majority? |
|
294 |
+ counts = {} |
|
295 |
+ options['hit']['questions'].each do |question| |
|
296 |
+ question_counts = question['selections'].inject({}) { |memo, selection| memo[selection['key']] = 0; memo } |
|
297 |
+ assignments.each do |assignment| |
|
298 |
+ answers = ActiveSupport::HashWithIndifferentAccess.new(assignment.answers) |
|
299 |
+ answer = answers[question['key']] |
|
300 |
+ question_counts[answer] += 1 |
|
208 | 301 |
end |
209 |
- memo[key] = sum / divisor.to_f |
|
302 |
+ counts[question['key']] = question_counts |
|
303 |
+ end |
|
304 |
+ payload['counts'] = counts |
|
305 |
+ |
|
306 |
+ majority_answer = counts.inject({}) do |memo, (key, question_counts)| |
|
307 |
+ memo[key] = question_counts.to_a.sort {|a, b| a.last <=> b.last }.last.first |
|
210 | 308 |
memo |
211 | 309 |
end |
212 |
- payload[:average_answer] = average_answer |
|
310 |
+ payload['majority_answer'] = majority_answer |
|
311 |
+ |
|
312 |
+ if all_questions_are_numeric? |
|
313 |
+ average_answer = counts.inject({}) do |memo, (key, question_counts)| |
|
314 |
+ sum = divisor = 0 |
|
315 |
+ question_counts.to_a.each do |num, count| |
|
316 |
+ sum += num.to_s.to_f * count |
|
317 |
+ divisor += count |
|
318 |
+ end |
|
319 |
+ memo[key] = sum / divisor.to_f |
|
320 |
+ memo |
|
321 |
+ end |
|
322 |
+ payload['average_answer'] = average_answer |
|
323 |
+ end |
|
213 | 324 |
end |
214 |
- end |
|
215 | 325 |
|
216 |
- event = create_event :payload => payload |
|
217 |
- log "Event emitted with answer(s)", :outbound_event => event, :inbound_event => Event.find_by_id(memory[:hits][hit_id.to_sym]) |
|
326 |
+ if create_poll? |
|
327 |
+ questions = [] |
|
328 |
+ selections = 5.times.map { |i| { 'key' => i+1, 'text' => i+1 } }.reverse |
|
329 |
+ assignments.length.times do |index| |
|
330 |
+ questions << { |
|
331 |
+ 'type' => "selection", |
|
332 |
+ 'name' => "Item #{index + 1}", |
|
333 |
+ 'key' => index, |
|
334 |
+ 'required' => "true", |
|
335 |
+ 'question' => Utils.interpolate_jsonpaths(options['poll_options']['row_template'], assignments[index].answers), |
|
336 |
+ 'selections' => selections |
|
337 |
+ } |
|
338 |
+ end |
|
339 |
+ |
|
340 |
+ poll_hit = create_hit 'title' => options['poll_options']['title'], |
|
341 |
+ 'description' => options['poll_options']['instructions'], |
|
342 |
+ 'questions' => questions, |
|
343 |
+ 'assignments' => options['poll_options']['assignments'], |
|
344 |
+ 'lifetime_in_seconds' => options['poll_options']['lifetime_in_seconds'], |
|
345 |
+ 'reward' => options['poll_options']['reward'], |
|
346 |
+ 'payload' => inbound_event && inbound_event.payload, |
|
347 |
+ 'metadata' => { 'type' => 'poll', |
|
348 |
+ 'original_hit' => hit_id, |
|
349 |
+ 'answers' => assignments.map(&:answers), |
|
350 |
+ 'event_id' => inbound_event && inbound_event.id } |
|
351 |
+ |
|
352 |
+ log "Poll HIT created with ID #{poll_hit.id} and URL #{poll_hit.url}. Original HIT: #{hit_id}", :inbound_event => inbound_event |
|
353 |
+ else |
|
354 |
+ event = create_event :payload => payload |
|
355 |
+ log "Event emitted with answer(s)", :outbound_event => event, :inbound_event => inbound_event |
|
356 |
+ end |
|
357 |
+ end |
|
218 | 358 |
|
219 | 359 |
assignments.each(&:approve!) |
220 | 360 |
hit.dispose! |
221 | 361 |
|
222 |
- memory[:hits].delete(hit_id.to_sym) |
|
362 |
+ memory['hits'].delete(hit_id) |
|
223 | 363 |
end |
224 | 364 |
end |
225 | 365 |
end |
226 | 366 |
|
227 | 367 |
def all_questions_are_numeric? |
228 |
- options[:hit][:questions].all? do |question| |
|
229 |
- question[:selections].all? do |selection| |
|
230 |
- selection[:key] == selection[:key].to_f.to_s || selection[:key] == selection[:key].to_i.to_s |
|
368 |
+ options['hit']['questions'].all? do |question| |
|
369 |
+ question['selections'].all? do |selection| |
|
370 |
+ selection['key'] == selection['key'].to_f.to_s || selection['key'] == selection['key'].to_i.to_s |
|
231 | 371 |
end |
232 | 372 |
end |
233 | 373 |
end |
234 | 374 |
|
235 |
- def create_hit(event = nil) |
|
236 |
- payload = event ? event.payload : {} |
|
237 |
- title = Utils.interpolate_jsonpaths(options[:hit][:title], payload).strip |
|
238 |
- description = Utils.interpolate_jsonpaths(options[:hit][:description], payload).strip |
|
239 |
- questions = Utils.recursively_interpolate_jsonpaths(options[:hit][:questions], payload) |
|
375 |
+ def create_basic_hit(event = nil) |
|
376 |
+ hit = create_hit 'title' => options['hit']['title'], |
|
377 |
+ 'description' => options['hit']['description'], |
|
378 |
+ 'questions' => options['hit']['questions'], |
|
379 |
+ 'assignments' => options['hit']['assignments'], |
|
380 |
+ 'lifetime_in_seconds' => options['hit']['lifetime_in_seconds'], |
|
381 |
+ 'reward' => options['hit']['reward'], |
|
382 |
+ 'payload' => event && event.payload, |
|
383 |
+ 'metadata' => { 'event_id' => event && event.id } |
|
384 |
+ |
|
385 |
+ log "HIT created with ID #{hit.id} and URL #{hit.url}", :inbound_event => event |
|
386 |
+ end |
|
387 |
+ |
|
388 |
+ def create_hit(opts = {}) |
|
389 |
+ payload = opts['payload'] || {} |
|
390 |
+ title = Utils.interpolate_jsonpaths(opts['title'], payload).strip |
|
391 |
+ description = Utils.interpolate_jsonpaths(opts['description'], payload).strip |
|
392 |
+ questions = Utils.recursively_interpolate_jsonpaths(opts['questions'], payload) |
|
240 | 393 |
hit = RTurk::Hit.create(:title => title) do |hit| |
241 |
- hit.max_assignments = (options[:hit][:assignments] || 1).to_i |
|
394 |
+ hit.max_assignments = (opts['assignments'] || 1).to_i |
|
242 | 395 |
hit.description = description |
243 |
- hit.lifetime = (options[:hit][:lifetime_in_seconds] || 24 * 60 * 60).to_i |
|
396 |
+ hit.lifetime = (opts['lifetime_in_seconds'] || 24 * 60 * 60).to_i |
|
244 | 397 |
hit.question_form AgentQuestionForm.new(:title => title, :description => description, :questions => questions) |
245 |
- hit.reward = (options[:hit][:reward] || 0.05).to_f |
|
398 |
+ hit.reward = (opts['reward'] || 0.05).to_f |
|
246 | 399 |
#hit.qualifications.add :approval_rate, { :gt => 80 } |
247 | 400 |
end |
248 |
- memory[:hits] ||= {} |
|
249 |
- memory[:hits][hit.id] = event && event.id |
|
250 |
- log "HIT created with ID #{hit.id} and URL #{hit.url}", :inbound_event => event |
|
401 |
+ memory['hits'] ||= {} |
|
402 |
+ memory['hits'][hit.id] = opts['metadata'] || {} |
|
403 |
+ hit |
|
251 | 404 |
end |
252 | 405 |
|
253 | 406 |
# RTurk Question Form |
@@ -268,34 +421,34 @@ module Agents |
||
268 | 421 |
@questions.each.with_index do |question, index| |
269 | 422 |
Question do |
270 | 423 |
QuestionIdentifier do |
271 |
- text question[:key] || "question_#{index}" |
|
424 |
+ text question['key'] || "question_#{index}" |
|
272 | 425 |
end |
273 | 426 |
DisplayName do |
274 |
- text question[:name] || "Question ##{index}" |
|
427 |
+ text question['name'] || "Question ##{index}" |
|
275 | 428 |
end |
276 | 429 |
IsRequired do |
277 |
- text question[:required] || 'true' |
|
430 |
+ text question['required'] || 'true' |
|
278 | 431 |
end |
279 | 432 |
QuestionContent do |
280 | 433 |
Text do |
281 |
- text question[:question] |
|
434 |
+ text question['question'] |
|
282 | 435 |
end |
283 | 436 |
end |
284 | 437 |
AnswerSpecification do |
285 |
- if question[:type] == "selection" |
|
438 |
+ if question['type'] == "selection" |
|
286 | 439 |
|
287 | 440 |
SelectionAnswer do |
288 | 441 |
StyleSuggestion do |
289 | 442 |
text 'radiobutton' |
290 | 443 |
end |
291 | 444 |
Selections do |
292 |
- question[:selections].each do |selection| |
|
445 |
+ question['selections'].each do |selection| |
|
293 | 446 |
Selection do |
294 | 447 |
SelectionIdentifier do |
295 |
- text selection[:key] |
|
448 |
+ text selection['key'] |
|
296 | 449 |
end |
297 | 450 |
Text do |
298 |
- text selection[:text] |
|
451 |
+ text selection['text'] |
|
299 | 452 |
end |
300 | 453 |
end |
301 | 454 |
end |
@@ -305,18 +458,18 @@ module Agents |
||
305 | 458 |
else |
306 | 459 |
|
307 | 460 |
FreeTextAnswer do |
308 |
- if question[:min_length].present? || question[:max_length].present? |
|
461 |
+ if question['min_length'].present? || question['max_length'].present? |
|
309 | 462 |
Constraints do |
310 | 463 |
lengths = {} |
311 |
- lengths[:minLength] = question[:min_length].to_s if question[:min_length].present? |
|
312 |
- lengths[:maxLength] = question[:max_length].to_s if question[:max_length].present? |
|
464 |
+ lengths['minLength'] = question['min_length'].to_s if question['min_length'].present? |
|
465 |
+ lengths['maxLength'] = question['max_length'].to_s if question['max_length'].present? |
|
313 | 466 |
Length lengths |
314 | 467 |
end |
315 | 468 |
end |
316 | 469 |
|
317 |
- if question[:default].present? |
|
470 |
+ if question['default'].present? |
|
318 | 471 |
DefaultText do |
319 |
- text question[:default] |
|
472 |
+ text question['default'] |
|
320 | 473 |
end |
321 | 474 |
end |
322 | 475 |
end |
@@ -328,4 +481,4 @@ module Agents |
||
328 | 481 |
end |
329 | 482 |
end |
330 | 483 |
end |
331 |
-end |
|
484 |
+end |
@@ -14,8 +14,8 @@ module Agents |
||
14 | 14 |
end |
15 | 15 |
|
16 | 16 |
def handle_details_post(params) |
17 |
- if params[:payload] |
|
18 |
- create_event(:payload => params[:payload]) |
|
17 |
+ if params['payload'] |
|
18 |
+ create_event(:payload => params['payload']) |
|
19 | 19 |
{ :success => true } |
20 | 20 |
else |
21 | 21 |
{ :success => false, :error => "You must provide a JSON payload" } |
@@ -28,22 +28,22 @@ module Agents |
||
28 | 28 |
MD |
29 | 29 |
|
30 | 30 |
def validate_options |
31 |
- unless options[:expected_receive_period_in_days].present? && options[:message].present? && options[:value_path].present? |
|
31 |
+ unless options['expected_receive_period_in_days'].present? && options['message'].present? && options['value_path'].present? |
|
32 | 32 |
errors.add(:base, "expected_receive_period_in_days, value_path, and message are required") |
33 | 33 |
end |
34 | 34 |
end |
35 | 35 |
|
36 | 36 |
def default_options |
37 | 37 |
{ |
38 |
- :expected_receive_period_in_days => "2", |
|
39 |
- :group_by_path => "filter", |
|
40 |
- :value_path => "count", |
|
41 |
- :message => "A peak was found" |
|
38 |
+ 'expected_receive_period_in_days' => "2", |
|
39 |
+ 'group_by_path' => "filter", |
|
40 |
+ 'value_path' => "count", |
|
41 |
+ 'message' => "A peak was found" |
|
42 | 42 |
} |
43 | 43 |
end |
44 | 44 |
|
45 | 45 |
def working? |
46 |
- last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? |
|
46 |
+ last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
47 | 47 |
end |
48 | 48 |
|
49 | 49 |
def receive(incoming_events) |
@@ -57,25 +57,23 @@ module Agents |
||
57 | 57 |
private |
58 | 58 |
|
59 | 59 |
def check_for_peak(group, event) |
60 |
- memory[:peaks] ||= {} |
|
61 |
- memory[:peaks][group] ||= [] |
|
60 |
+ memory['peaks'] ||= {} |
|
61 |
+ memory['peaks'][group] ||= [] |
|
62 | 62 |
|
63 |
- if memory[:data][group].length > 4 && (memory[:peaks][group].empty? || memory[:peaks][group].last < event.created_at.to_i - peak_spacing) |
|
63 |
+ if memory['data'][group].length > 4 && (memory['peaks'][group].empty? || memory['peaks'][group].last < event.created_at.to_i - peak_spacing) |
|
64 | 64 |
average_value, standard_deviation = stats_for(group, :skip_last => 1) |
65 |
- newest_value, newest_time = memory[:data][group][-1].map(&:to_f) |
|
66 |
- |
|
67 |
- #p [newest_value, average_value, average_value + std_multiple * standard_deviation, standard_deviation] |
|
65 |
+ newest_value, newest_time = memory['data'][group][-1].map(&:to_f) |
|
68 | 66 |
|
69 | 67 |
if newest_value > average_value + std_multiple * standard_deviation |
70 |
- memory[:peaks][group] << newest_time |
|
71 |
- memory[:peaks][group].reject! { |p| p <= newest_time - window_duration } |
|
72 |
- create_event :payload => {:message => options[:message], :peak => newest_value, :peak_time => newest_time, :grouped_by => group.to_s} |
|
68 |
+ memory['peaks'][group] << newest_time |
|
69 |
+ memory['peaks'][group].reject! { |p| p <= newest_time - window_duration } |
|
70 |
+ create_event :payload => { 'message' => options['message'], 'peak' => newest_value, 'peak_time' => newest_time, 'grouped_by' => group.to_s } |
|
73 | 71 |
end |
74 | 72 |
end |
75 | 73 |
end |
76 | 74 |
|
77 | 75 |
def stats_for(group, options = {}) |
78 |
- data = memory[:data][group].map { |d| d.first.to_f } |
|
76 |
+ data = memory['data'][group].map { |d| d.first.to_f } |
|
79 | 77 |
data = data[0...(data.length - (options[:skip_last] || 0))] |
80 | 78 |
length = data.length.to_f |
81 | 79 |
mean = 0 |
@@ -94,39 +92,39 @@ module Agents |
||
94 | 92 |
end |
95 | 93 |
|
96 | 94 |
def window_duration |
97 |
- if options[:window_duration].present? # The older option |
|
98 |
- options[:window_duration].to_i |
|
95 |
+ if options['window_duration'].present? # The older option |
|
96 |
+ options['window_duration'].to_i |
|
99 | 97 |
else |
100 |
- (options[:window_duration_in_days] || 14).to_f.days |
|
98 |
+ (options['window_duration_in_days'] || 14).to_f.days |
|
101 | 99 |
end |
102 | 100 |
end |
103 | 101 |
|
104 | 102 |
def std_multiple |
105 |
- (options[:std_multiple] || 3).to_f |
|
103 |
+ (options['std_multiple'] || 3).to_f |
|
106 | 104 |
end |
107 | 105 |
|
108 | 106 |
def peak_spacing |
109 |
- if options[:peak_spacing].present? # The older option |
|
110 |
- options[:peak_spacing].to_i |
|
107 |
+ if options['peak_spacing'].present? # The older option |
|
108 |
+ options['peak_spacing'].to_i |
|
111 | 109 |
else |
112 |
- (options[:min_peak_spacing_in_days] || 2).to_f.days |
|
110 |
+ (options['min_peak_spacing_in_days'] || 2).to_f.days |
|
113 | 111 |
end |
114 | 112 |
end |
115 | 113 |
|
116 | 114 |
def group_for(event) |
117 |
- ((options[:group_by_path].present? && Utils.value_at(event.payload, options[:group_by_path])) || 'no_group').to_sym |
|
115 |
+ ((options['group_by_path'].present? && Utils.value_at(event.payload, options['group_by_path'])) || 'no_group') |
|
118 | 116 |
end |
119 | 117 |
|
120 | 118 |
def remember(group, event) |
121 |
- memory[:data] ||= {} |
|
122 |
- memory[:data][group] ||= [] |
|
123 |
- memory[:data][group] << [Utils.value_at(event.payload, options[:value_path]), event.created_at.to_i] |
|
119 |
+ memory['data'] ||= {} |
|
120 |
+ memory['data'][group] ||= [] |
|
121 |
+ memory['data'][group] << [ Utils.value_at(event.payload, options['value_path']), event.created_at.to_i ] |
|
124 | 122 |
cleanup group |
125 | 123 |
end |
126 | 124 |
|
127 | 125 |
def cleanup(group) |
128 |
- newest_time = memory[:data][group].last.last |
|
129 |
- memory[:data][group].reject! { |value, time| time <= newest_time - window_duration } |
|
126 |
+ newest_time = memory['data'][group].last.last |
|
127 |
+ memory['data'][group].reject! { |value, time| time <= newest_time - window_duration } |
|
130 | 128 |
end |
131 | 129 |
end |
132 | 130 |
end |
@@ -11,17 +11,17 @@ module Agents |
||
11 | 11 |
|
12 | 12 |
def default_options |
13 | 13 |
{ |
14 |
- :post_url => "http://www.example.com", |
|
15 |
- :expected_receive_period_in_days => 1 |
|
14 |
+ 'post_url' => "http://www.example.com", |
|
15 |
+ 'expected_receive_period_in_days' => 1 |
|
16 | 16 |
} |
17 | 17 |
end |
18 | 18 |
|
19 | 19 |
def working? |
20 |
- last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? |
|
20 |
+ last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
21 | 21 |
end |
22 | 22 |
|
23 | 23 |
def validate_options |
24 |
- unless options[:post_url].present? && options[:expected_receive_period_in_days].present? |
|
24 |
+ unless options['post_url'].present? && options['expected_receive_period_in_days'].present? |
|
25 | 25 |
errors.add(:base, "post_url and expected_receive_period_in_days are required fields") |
26 | 26 |
end |
27 | 27 |
end |
@@ -28,31 +28,31 @@ module Agents |
||
28 | 28 |
|
29 | 29 |
def default_options |
30 | 30 |
{ |
31 |
- :content => "$.message.text[*]", |
|
32 |
- :expected_receive_period_in_days => 1 |
|
31 |
+ 'content' => "$.message.text[*]", |
|
32 |
+ 'expected_receive_period_in_days' => 1 |
|
33 | 33 |
} |
34 | 34 |
end |
35 | 35 |
|
36 | 36 |
def working? |
37 |
- last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? |
|
37 |
+ last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
38 | 38 |
end |
39 | 39 |
|
40 | 40 |
def receive(incoming_events) |
41 | 41 |
anew = self.class.sentiment_hash |
42 | 42 |
incoming_events.each do |event| |
43 |
- Utils.values_at(event.payload, options[:content]).each do |content| |
|
43 |
+ Utils.values_at(event.payload, options['content']).each do |content| |
|
44 | 44 |
sent_values = sentiment_values anew, content |
45 |
- create_event :payload => { :content => content, |
|
46 |
- :valence => sent_values[0], |
|
47 |
- :arousal => sent_values[1], |
|
48 |
- :dominance => sent_values[2], |
|
49 |
- :original_event => event.payload } |
|
45 |
+ create_event :payload => { 'content' => content, |
|
46 |
+ 'valence' => sent_values[0], |
|
47 |
+ 'arousal' => sent_values[1], |
|
48 |
+ 'dominance' => sent_values[2], |
|
49 |
+ 'original_event' => event.payload } |
|
50 | 50 |
end |
51 | 51 |
end |
52 | 52 |
end |
53 | 53 |
|
54 | 54 |
def validate_options |
55 |
- errors.add(:base, "content and expected_receive_period_in_days must be present") unless options[:content].present? && options[:expected_receive_period_in_days].present? |
|
55 |
+ errors.add(:base, "content and expected_receive_period_in_days must be present") unless options['content'].present? && options['expected_receive_period_in_days'].present? |
|
56 | 56 |
end |
57 | 57 |
|
58 | 58 |
def self.sentiment_hash |
@@ -17,26 +17,26 @@ module Agents |
||
17 | 17 |
|
18 | 18 |
def default_options |
19 | 19 |
{ |
20 |
- :client_id => "xxxxxx", |
|
21 |
- :client_secret => "xxxxxx", |
|
22 |
- :to => "fi", |
|
23 |
- :expected_receive_period_in_days => 1, |
|
24 |
- :content => { |
|
25 |
- :text => "$.message.text", |
|
26 |
- :content => "$.xyz" |
|
20 |
+ 'client_id' => "xxxxxx", |
|
21 |
+ 'client_secret' => "xxxxxx", |
|
22 |
+ 'to' => "fi", |
|
23 |
+ 'expected_receive_period_in_days' => 1, |
|
24 |
+ 'content' => { |
|
25 |
+ 'text' => "$.message.text", |
|
26 |
+ 'content' => "$.xyz" |
|
27 | 27 |
} |
28 | 28 |
} |
29 | 29 |
end |
30 | 30 |
|
31 | 31 |
def working? |
32 |
- last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? |
|
32 |
+ last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
33 | 33 |
end |
34 | 34 |
|
35 | 35 |
def translate(text, to, access_token) |
36 | 36 |
translate_uri = URI 'http://api.microsofttranslator.com/v2/Ajax.svc/Translate' |
37 | 37 |
params = { |
38 |
- :text => text, |
|
39 |
- :to => to |
|
38 |
+ 'text' => text, |
|
39 |
+ 'to' => to |
|
40 | 40 |
} |
41 | 41 |
translate_uri.query = URI.encode_www_form params |
42 | 42 |
request = Net::HTTP::Get.new translate_uri.request_uri |
@@ -47,7 +47,7 @@ module Agents |
||
47 | 47 |
end |
48 | 48 |
|
49 | 49 |
def validate_options |
50 |
- unless options[:client_id].present? && options[:client_secret].present? && options[:to].present? && options[:content].present? && options[:expected_receive_period_in_days].present? |
|
50 |
+ unless options['client_id'].present? && options['client_secret'].present? && options['to'].present? && options['content'].present? && options['expected_receive_period_in_days'].present? |
|
51 | 51 |
errors.add :base, "client_id,client_secret,to,expected_receive_period_in_days and content are all required" |
52 | 52 |
end |
53 | 53 |
end |
@@ -60,16 +60,16 @@ module Agents |
||
60 | 60 |
|
61 | 61 |
def receive(incoming_events) |
62 | 62 |
auth_uri = URI "https://datamarket.accesscontrol.windows.net/v2/OAuth2-13" |
63 |
- response = postform auth_uri, :client_id => options[:client_id], |
|
64 |
- :client_secret => options[:client_secret], |
|
63 |
+ response = postform auth_uri, :client_id => options['client_id'], |
|
64 |
+ :client_secret => options['client_secret'], |
|
65 | 65 |
:scope => "http://api.microsofttranslator.com", |
66 | 66 |
:grant_type => "client_credentials" |
67 | 67 |
access_token = JSON.parse(response.body)["access_token"] |
68 | 68 |
incoming_events.each do |event| |
69 | 69 |
translated_event = {} |
70 |
- options[:content].each_pair do |key, value| |
|
70 |
+ options['content'].each_pair do |key, value| |
|
71 | 71 |
to_be_translated = Utils.values_at event.payload, value |
72 |
- translated_event[key] = translate to_be_translated.first, options[:to], access_token |
|
72 |
+ translated_event[key] = translate to_be_translated.first, options['to'], access_token |
|
73 | 73 |
end |
74 | 74 |
create_event :payload => translated_event |
75 | 75 |
end |
@@ -23,57 +23,57 @@ module Agents |
||
23 | 23 |
MD |
24 | 24 |
|
25 | 25 |
def validate_options |
26 |
- unless options[:expected_receive_period_in_days].present? && options[:message].present? && options[:rules].present? && |
|
27 |
- options[:rules].all? { |rule| rule[:type].present? && VALID_COMPARISON_TYPES.include?(rule[:type]) && rule[:value].present? && rule[:path].present? } |
|
26 |
+ unless options['expected_receive_period_in_days'].present? && options['message'].present? && options['rules'].present? && |
|
27 |
+ options['rules'].all? { |rule| rule['type'].present? && VALID_COMPARISON_TYPES.include?(rule['type']) && rule['value'].present? && rule['path'].present? } |
|
28 | 28 |
errors.add(:base, "expected_receive_period_in_days, message, and rules, with a type, value, and path for every rule, are required") |
29 | 29 |
end |
30 | 30 |
end |
31 | 31 |
|
32 | 32 |
def default_options |
33 | 33 |
{ |
34 |
- :expected_receive_period_in_days => "2", |
|
35 |
- :rules => [{ |
|
36 |
- :type => "regex", |
|
37 |
- :value => "foo\\d+bar", |
|
38 |
- :path => "topkey.subkey.subkey.goal", |
|
39 |
- }], |
|
40 |
- :message => "Looks like your pattern matched in '<value>'!" |
|
34 |
+ 'expected_receive_period_in_days' => "2", |
|
35 |
+ 'rules' => [{ |
|
36 |
+ 'type' => "regex", |
|
37 |
+ 'value' => "foo\\d+bar", |
|
38 |
+ 'path' => "topkey.subkey.subkey.goal", |
|
39 |
+ }], |
|
40 |
+ 'message' => "Looks like your pattern matched in '<value>'!" |
|
41 | 41 |
} |
42 | 42 |
end |
43 | 43 |
|
44 | 44 |
def working? |
45 |
- last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? |
|
45 |
+ last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
46 | 46 |
end |
47 | 47 |
|
48 | 48 |
def receive(incoming_events) |
49 | 49 |
incoming_events.each do |event| |
50 |
- match = options[:rules].all? do |rule| |
|
51 |
- value_at_path = Utils.value_at(event[:payload], rule[:path]) |
|
52 |
- case rule[:type] |
|
50 |
+ match = options['rules'].all? do |rule| |
|
51 |
+ value_at_path = Utils.value_at(event['payload'], rule['path']) |
|
52 |
+ case rule['type'] |
|
53 | 53 |
when "regex" |
54 |
- value_at_path.to_s =~ Regexp.new(rule[:value], Regexp::IGNORECASE) |
|
54 |
+ value_at_path.to_s =~ Regexp.new(rule['value'], Regexp::IGNORECASE) |
|
55 | 55 |
when "!regex" |
56 |
- value_at_path.to_s !~ Regexp.new(rule[:value], Regexp::IGNORECASE) |
|
56 |
+ value_at_path.to_s !~ Regexp.new(rule['value'], Regexp::IGNORECASE) |
|
57 | 57 |
when "field>value" |
58 |
- value_at_path.to_f > rule[:value].to_f |
|
58 |
+ value_at_path.to_f > rule['value'].to_f |
|
59 | 59 |
when "field>=value" |
60 |
- value_at_path.to_f >= rule[:value].to_f |
|
60 |
+ value_at_path.to_f >= rule['value'].to_f |
|
61 | 61 |
when "field<value" |
62 |
- value_at_path.to_f < rule[:value].to_f |
|
62 |
+ value_at_path.to_f < rule['value'].to_f |
|
63 | 63 |
when "field<=value" |
64 |
- value_at_path.to_f <= rule[:value].to_f |
|
64 |
+ value_at_path.to_f <= rule['value'].to_f |
|
65 | 65 |
when "field==value" |
66 |
- value_at_path.to_s == rule[:value].to_s |
|
66 |
+ value_at_path.to_s == rule['value'].to_s |
|
67 | 67 |
when "field!=value" |
68 |
- value_at_path.to_s != rule[:value].to_s |
|
68 |
+ value_at_path.to_s != rule['value'].to_s |
|
69 | 69 |
else |
70 |
- raise "Invalid :type of #{rule[:type]} in TriggerAgent##{id}" |
|
70 |
+ raise "Invalid type of #{rule['type']} in TriggerAgent##{id}" |
|
71 | 71 |
end |
72 | 72 |
end |
73 | 73 |
|
74 | 74 |
if match |
75 |
- create_event :payload => { :message => make_message(event[:payload]) } # Maybe this should include the |
|
76 |
- # original event as well? |
|
75 |
+ create_event :payload => { 'message' => make_message(event[:payload]) } # Maybe this should include the |
|
76 |
+ # original event as well? |
|
77 | 77 |
end |
78 | 78 |
end |
79 | 79 |
end |
@@ -9,7 +9,7 @@ module Agents |
||
9 | 9 |
description <<-MD |
10 | 10 |
The TwilioAgent receives and collects events and sends them via text message or gives you a call when scheduled. |
11 | 11 |
|
12 |
- It is assumed that events have a `:message`, `:text`, or `:sms` key, the value of which is sent as the content of the text message/call. You can use Event Formatting Agent if your event does not provide these keys. |
|
12 |
+ It is assumed that events have a `message`, `text`, or `sms` key, the value of which is sent as the content of the text message/call. You can use Event Formatting Agent if your event does not provide these keys. |
|
13 | 13 |
|
14 | 14 |
Set `receiver_cell` to the number to receive text messages/call and `sender_cell` to the number sending them. |
15 | 15 |
|
@@ -22,35 +22,35 @@ module Agents |
||
22 | 22 |
|
23 | 23 |
def default_options |
24 | 24 |
{ |
25 |
- :account_sid => 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', |
|
26 |
- :auth_token => 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', |
|
27 |
- :sender_cell => 'xxxxxxxxxx', |
|
28 |
- :receiver_cell => 'xxxxxxxxxx', |
|
29 |
- :server_url => 'http://somename.com:3000', |
|
30 |
- :receive_text => 'true', |
|
31 |
- :receive_call => 'false', |
|
32 |
- :expected_receive_period_in_days => '1' |
|
25 |
+ 'account_sid' => 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', |
|
26 |
+ 'auth_token' => 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', |
|
27 |
+ 'sender_cell' => 'xxxxxxxxxx', |
|
28 |
+ 'receiver_cell' => 'xxxxxxxxxx', |
|
29 |
+ 'server_url' => 'http://somename.com:3000', |
|
30 |
+ 'receive_text' => 'true', |
|
31 |
+ 'receive_call' => 'false', |
|
32 |
+ 'expected_receive_period_in_days' => '1' |
|
33 | 33 |
} |
34 | 34 |
end |
35 | 35 |
|
36 | 36 |
def validate_options |
37 |
- unless options[:account_sid].present? && options[:auth_token].present? && options[:sender_cell].present? && options[:receiver_cell].present? && options[:expected_receive_period_in_days].present? && options[:receive_call].present? && options[:receive_text].present? |
|
37 |
+ unless options['account_sid'].present? && options['auth_token'].present? && options['sender_cell'].present? && options['receiver_cell'].present? && options['expected_receive_period_in_days'].present? && options['receive_call'].present? && options['receive_text'].present? |
|
38 | 38 |
errors.add(:base, 'account_sid, auth_token, sender_cell, receiver_cell, receive_text, receive_call and expected_receive_period_in_days are all required') |
39 | 39 |
end |
40 | 40 |
end |
41 | 41 |
|
42 | 42 |
def receive(incoming_events) |
43 |
- @client = Twilio::REST::Client.new options[:account_sid], options[:auth_token] |
|
44 |
- memory[:pending_calls] ||= {} |
|
43 |
+ @client = Twilio::REST::Client.new options['account_sid'], options['auth_token'] |
|
44 |
+ memory['pending_calls'] ||= {} |
|
45 | 45 |
incoming_events.each do |event| |
46 |
- message = (event.payload[:message] || event.payload[:text] || event.payload[:sms]).to_s |
|
46 |
+ message = (event.payload['message'] || event.payload['text'] || event.payload['sms']).to_s |
|
47 | 47 |
if message != "" |
48 |
- if options[:receive_call].to_s == 'true' |
|
48 |
+ if options['receive_call'].to_s == 'true' |
|
49 | 49 |
secret = SecureRandom.hex 3 |
50 |
- memory[:pending_calls][secret] = message |
|
50 |
+ memory['pending_calls'][secret] = message |
|
51 | 51 |
make_call secret |
52 | 52 |
end |
53 |
- if options[:receive_text].to_s == 'true' |
|
53 |
+ if options['receive_text'].to_s == 'true' |
|
54 | 54 |
message = message.slice 0..160 |
55 | 55 |
send_message message |
56 | 56 |
end |
@@ -59,19 +59,19 @@ module Agents |
||
59 | 59 |
end |
60 | 60 |
|
61 | 61 |
def working? |
62 |
- last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? |
|
62 |
+ last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
63 | 63 |
end |
64 | 64 |
|
65 | 65 |
def send_message(message) |
66 |
- @client.account.sms.messages.create :from => options[:sender_cell], |
|
67 |
- :to => options[:receiver_cell], |
|
66 |
+ @client.account.sms.messages.create :from => options['sender_cell'], |
|
67 |
+ :to => options['receiver_cell'], |
|
68 | 68 |
:body => message |
69 | 69 |
end |
70 | 70 |
|
71 | 71 |
def make_call(secret) |
72 |
- @client.account.calls.create :from => options[:sender_cell], |
|
73 |
- :to => options[:receiver_cell], |
|
74 |
- :url => post_url(options[:server_url],secret) |
|
72 |
+ @client.account.calls.create :from => options['sender_cell'], |
|
73 |
+ :to => options['receiver_cell'], |
|
74 |
+ :url => post_url(options['server_url'],secret) |
|
75 | 75 |
end |
76 | 76 |
|
77 | 77 |
def post_url(server_url,secret) |
@@ -79,9 +79,9 @@ module Agents |
||
79 | 79 |
end |
80 | 80 |
|
81 | 81 |
def receive_webhook(params) |
82 |
- if memory[:pending_calls].has_key? params[:secret].to_sym |
|
83 |
- response = Twilio::TwiML::Response.new {|r| r.Say memory[:pending_calls][params[:secret].to_sym], :voice => 'woman'} |
|
84 |
- memory[:pending_calls].delete params[:secret].to_sym |
|
82 |
+ if memory['pending_calls'].has_key? params['secret'] |
|
83 |
+ response = Twilio::TwiML::Response.new {|r| r.Say memory['pending_calls'][params['secret']], :voice => 'woman'} |
|
84 |
+ memory['pending_calls'].delete params['secret'] |
|
85 | 85 |
[response.text, 200] |
86 | 86 |
end |
87 | 87 |
end |
@@ -19,25 +19,25 @@ module Agents |
||
19 | 19 |
MD |
20 | 20 |
|
21 | 21 |
def validate_options |
22 |
- unless options[:username].present? && |
|
23 |
- options[:expected_update_period_in_days].present? |
|
22 |
+ unless options['username'].present? && |
|
23 |
+ options['expected_update_period_in_days'].present? |
|
24 | 24 |
errors.add(:base, "username and expected_update_period_in_days are required") |
25 | 25 |
end |
26 | 26 |
end |
27 | 27 |
|
28 | 28 |
def working? |
29 |
- (event = event_created_within(options[:expected_update_period_in_days])) && event.payload[:success] == true && !recent_error_logs? |
|
29 |
+ event_created_within?(options['expected_update_period_in_days']) && most_recent_event.payload['success'] == true && !recent_error_logs? |
|
30 | 30 |
end |
31 | 31 |
|
32 | 32 |
def default_options |
33 | 33 |
{ |
34 |
- :username => "", |
|
35 |
- :expected_update_period_in_days => "10", |
|
36 |
- :consumer_key => "---", |
|
37 |
- :consumer_secret => "---", |
|
38 |
- :oauth_token => "---", |
|
39 |
- :oauth_token_secret => "---", |
|
40 |
- :message_path => "text" |
|
34 |
+ 'username' => "", |
|
35 |
+ 'expected_update_period_in_days' => "10", |
|
36 |
+ 'consumer_key' => "---", |
|
37 |
+ 'consumer_secret' => "---", |
|
38 |
+ 'oauth_token' => "---", |
|
39 |
+ 'oauth_token_secret' => "---", |
|
40 |
+ 'message_path' => "text" |
|
41 | 41 |
} |
42 | 42 |
end |
43 | 43 |
|
@@ -47,22 +47,22 @@ module Agents |
||
47 | 47 |
incoming_events = incoming_events.first(20) |
48 | 48 |
end |
49 | 49 |
incoming_events.each do |event| |
50 |
- tweet_text = Utils.value_at(event.payload, options[:message_path]) |
|
50 |
+ tweet_text = Utils.value_at(event.payload, options['message_path']) |
|
51 | 51 |
begin |
52 | 52 |
publish_tweet tweet_text |
53 | 53 |
create_event :payload => { |
54 |
- :success => true, |
|
55 |
- :published_tweet => tweet_text, |
|
56 |
- :agent_id => event.agent_id, |
|
57 |
- :event_id => event.id |
|
54 |
+ 'success' => true, |
|
55 |
+ 'published_tweet' => tweet_text, |
|
56 |
+ 'agent_id' => event.agent_id, |
|
57 |
+ 'event_id' => event.id |
|
58 | 58 |
} |
59 | 59 |
rescue Twitter::Error => e |
60 | 60 |
create_event :payload => { |
61 |
- :success => false, |
|
62 |
- :error => e.message, |
|
63 |
- :failed_tweet => tweet_text, |
|
64 |
- :agent_id => event.agent_id, |
|
65 |
- :event_id => event.id |
|
61 |
+ 'success' => false, |
|
62 |
+ 'error' => e.message, |
|
63 |
+ 'failed_tweet' => tweet_text, |
|
64 |
+ 'agent_id' => event.agent_id, |
|
65 |
+ 'event_id' => event.id |
|
66 | 66 |
} |
67 | 67 |
end |
68 | 68 |
end |
@@ -54,26 +54,26 @@ module Agents |
||
54 | 54 |
default_schedule "11pm" |
55 | 55 |
|
56 | 56 |
def validate_options |
57 |
- unless options[:filters].present? && |
|
58 |
- options[:expected_update_period_in_days].present? && |
|
59 |
- options[:generate].present? |
|
57 |
+ unless options['filters'].present? && |
|
58 |
+ options['expected_update_period_in_days'].present? && |
|
59 |
+ options['generate'].present? |
|
60 | 60 |
errors.add(:base, "expected_update_period_in_days, generate, and filters are required fields") |
61 | 61 |
end |
62 | 62 |
end |
63 | 63 |
|
64 | 64 |
def working? |
65 |
- event_created_within(options[:expected_update_period_in_days]) && !recent_error_logs? |
|
65 |
+ event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? |
|
66 | 66 |
end |
67 | 67 |
|
68 | 68 |
def default_options |
69 | 69 |
{ |
70 |
- :consumer_key => "---", |
|
71 |
- :consumer_secret => "---", |
|
72 |
- :oauth_token => "---", |
|
73 |
- :oauth_token_secret => "---", |
|
74 |
- :filters => %w[keyword1 keyword2], |
|
75 |
- :expected_update_period_in_days => "2", |
|
76 |
- :generate => "events" |
|
70 |
+ 'consumer_key' => "---", |
|
71 |
+ 'consumer_secret' => "---", |
|
72 |
+ 'oauth_token' => "---", |
|
73 |
+ 'oauth_token_secret' => "---", |
|
74 |
+ 'filters' => %w[keyword1 keyword2], |
|
75 |
+ 'expected_update_period_in_days' => "2", |
|
76 |
+ 'generate' => "events" |
|
77 | 77 |
} |
78 | 78 |
end |
79 | 79 |
|
@@ -81,33 +81,33 @@ module Agents |
||
81 | 81 |
filter = lookup_filter(filter) |
82 | 82 |
|
83 | 83 |
if filter |
84 |
- if options[:generate] == "counts" |
|
84 |
+ if options['generate'] == "counts" |
|
85 | 85 |
# Avoid memory pollution by reloading the Agent. |
86 | 86 |
agent = Agent.find(id) |
87 |
- agent.memory[:filter_counts] ||= {} |
|
88 |
- agent.memory[:filter_counts][filter.to_sym] ||= 0 |
|
89 |
- agent.memory[:filter_counts][filter.to_sym] += 1 |
|
90 |
- remove_unused_keys!(agent, :filter_counts) |
|
87 |
+ agent.memory['filter_counts'] ||= {} |
|
88 |
+ agent.memory['filter_counts'][filter] ||= 0 |
|
89 |
+ agent.memory['filter_counts'][filter] += 1 |
|
90 |
+ remove_unused_keys!(agent, 'filter_counts') |
|
91 | 91 |
agent.save! |
92 | 92 |
else |
93 |
- create_event :payload => status.merge(:filter => filter.to_s) |
|
93 |
+ create_event :payload => status.merge('filter' => filter) |
|
94 | 94 |
end |
95 | 95 |
end |
96 | 96 |
end |
97 | 97 |
|
98 | 98 |
def check |
99 |
- if options[:generate] == "counts" && memory[:filter_counts] && memory[:filter_counts].length > 0 |
|
100 |
- memory[:filter_counts].each do |filter, count| |
|
101 |
- create_event :payload => { :filter => filter.to_s, :count => count, :time => Time.now.to_i } |
|
99 |
+ if options['generate'] == "counts" && memory['filter_counts'] && memory['filter_counts'].length > 0 |
|
100 |
+ memory['filter_counts'].each do |filter, count| |
|
101 |
+ create_event :payload => { 'filter' => filter, 'count' => count, 'time' => Time.now.to_i } |
|
102 | 102 |
end |
103 | 103 |
end |
104 |
- memory[:filter_counts] = {} |
|
104 |
+ memory['filter_counts'] = {} |
|
105 | 105 |
end |
106 | 106 |
|
107 | 107 |
protected |
108 | 108 |
|
109 | 109 |
def lookup_filter(filter) |
110 |
- options[:filters].each do |known_filter| |
|
110 |
+ options['filters'].each do |known_filter| |
|
111 | 111 |
if known_filter == filter |
112 | 112 |
return filter |
113 | 113 |
elsif known_filter.is_a?(Array) |
@@ -120,7 +120,7 @@ module Agents |
||
120 | 120 |
|
121 | 121 |
def remove_unused_keys!(agent, base) |
122 | 122 |
if agent.memory[base] |
123 |
- (agent.memory[base].keys - agent.options[:filters].map {|f| f.is_a?(Array) ? f.first.to_sym : f.to_sym }).each do |removed_key| |
|
123 |
+ (agent.memory[base].keys - agent.options['filters'].map {|f| f.is_a?(Array) ? f.first.to_s : f.to_s }).each do |removed_key| |
|
124 | 124 |
agent.memory[base].delete(removed_key) |
125 | 125 |
end |
126 | 126 |
end |
@@ -41,36 +41,36 @@ module Agents |
||
41 | 41 |
default_schedule "every_1h" |
42 | 42 |
|
43 | 43 |
def validate_options |
44 |
- unless options[:username].present? && |
|
45 |
- options[:expected_update_period_in_days].present? |
|
44 |
+ unless options['username'].present? && |
|
45 |
+ options['expected_update_period_in_days'].present? |
|
46 | 46 |
errors.add(:base, "username and expected_update_period_in_days are required") |
47 | 47 |
end |
48 | 48 |
end |
49 | 49 |
|
50 | 50 |
def working? |
51 |
- event_created_within(options[:expected_update_period_in_days]) && !recent_error_logs? |
|
51 |
+ event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? |
|
52 | 52 |
end |
53 | 53 |
|
54 | 54 |
def default_options |
55 | 55 |
{ |
56 |
- :username => "tectonic", |
|
57 |
- :expected_update_period_in_days => "2", |
|
58 |
- :consumer_key => "---", |
|
59 |
- :consumer_secret => "---", |
|
60 |
- :oauth_token => "---", |
|
61 |
- :oauth_token_secret => "---" |
|
56 |
+ 'username' => "tectonic", |
|
57 |
+ 'expected_update_period_in_days' => "2", |
|
58 |
+ 'consumer_key' => "---", |
|
59 |
+ 'consumer_secret' => "---", |
|
60 |
+ 'oauth_token' => "---", |
|
61 |
+ 'oauth_token_secret' => "---" |
|
62 | 62 |
} |
63 | 63 |
end |
64 | 64 |
|
65 | 65 |
def check |
66 |
- since_id = memory[:since_id] || nil |
|
66 |
+ since_id = memory['since_id'] || nil |
|
67 | 67 |
opts = {:count => 200, :include_rts => true, :exclude_replies => false, :include_entities => true, :contributor_details => true} |
68 | 68 |
opts.merge! :since_id => since_id unless since_id.nil? |
69 | 69 |
|
70 |
- tweets = Twitter.user_timeline(options[:username], opts) |
|
70 |
+ tweets = Twitter.user_timeline(options['username'], opts) |
|
71 | 71 |
|
72 | 72 |
tweets.each do |tweet| |
73 |
- memory[:since_id] = tweet.id if !memory[:since_id] || (tweet.id > memory[:since_id]) |
|
73 |
+ memory['since_id'] = tweet.id if !memory['since_id'] || (tweet.id > memory['since_id']) |
|
74 | 74 |
|
75 | 75 |
create_event :payload => tweet.attrs |
76 | 76 |
end |
@@ -30,15 +30,15 @@ module Agents |
||
30 | 30 |
MD |
31 | 31 |
|
32 | 32 |
def working? |
33 |
- event_created_within(2) && !recent_error_logs? |
|
33 |
+ event_created_within?(2) && !recent_error_logs? |
|
34 | 34 |
end |
35 | 35 |
|
36 | 36 |
def default_options |
37 |
- { :secret => SecureRandom.hex(7) } |
|
37 |
+ { 'secret' => SecureRandom.hex(7) } |
|
38 | 38 |
end |
39 | 39 |
|
40 | 40 |
def validate_options |
41 |
- errors.add(:base, "secret is required and must be longer than 4 characters") unless options[:secret].present? && options[:secret].length > 4 |
|
41 |
+ errors.add(:base, "secret is required and must be longer than 4 characters") unless options['secret'].present? && options['secret'].length > 4 |
|
42 | 42 |
end |
43 | 43 |
end |
44 | 44 |
end |
@@ -41,34 +41,34 @@ module Agents |
||
41 | 41 |
default_schedule "8pm" |
42 | 42 |
|
43 | 43 |
def working? |
44 |
- event_created_within(2) && !recent_error_logs? |
|
44 |
+ event_created_within?(2) && !recent_error_logs? |
|
45 | 45 |
end |
46 | 46 |
|
47 | 47 |
def wunderground |
48 |
- Wunderground.new(options[:api_key]) if key_setup? |
|
48 |
+ Wunderground.new(options['api_key']) if key_setup? |
|
49 | 49 |
end |
50 | 50 |
|
51 | 51 |
def key_setup? |
52 |
- options[:api_key] && options[:api_key] != "your-key" |
|
52 |
+ options['api_key'] && options['api_key'] != "your-key" |
|
53 | 53 |
end |
54 | 54 |
|
55 | 55 |
def default_options |
56 | 56 |
{ |
57 |
- :api_key => "your-key", |
|
58 |
- :location => "94103" |
|
57 |
+ 'api_key' => "your-key", |
|
58 |
+ 'location' => "94103" |
|
59 | 59 |
} |
60 | 60 |
end |
61 | 61 |
|
62 | 62 |
def validate_options |
63 |
- errors.add(:base, "location is required") unless options[:location].present? || options[:zipcode].present? |
|
64 |
- errors.add(:base, "api_key is required") unless options[:api_key].present? |
|
63 |
+ errors.add(:base, "location is required") unless options['location'].present? || options['zipcode'].present? |
|
64 |
+ errors.add(:base, "api_key is required") unless options['api_key'].present? |
|
65 | 65 |
end |
66 | 66 |
|
67 | 67 |
def check |
68 | 68 |
if key_setup? |
69 |
- wunderground.forecast_for(options[:location] || options[:zipcode])["forecast"]["simpleforecast"]["forecastday"].each do |day| |
|
69 |
+ wunderground.forecast_for(options['location'] || options['zipcode'])["forecast"]["simpleforecast"]["forecastday"].each do |day| |
|
70 | 70 |
if is_tomorrow?(day) |
71 |
- create_event :payload => day.merge(:location => options[:location] || options[:zipcode]) |
|
71 |
+ create_event :payload => day.merge('location' => options['location'] || options['zipcode']) |
|
72 | 72 |
end |
73 | 73 |
end |
74 | 74 |
end |
@@ -0,0 +1,62 @@ |
||
1 |
+module Agents |
|
2 |
+ class WebhookAgent < Agent |
|
3 |
+ cannot_be_scheduled! |
|
4 |
+ |
|
5 |
+ description do |
|
6 |
+ <<-MD |
|
7 |
+ Use this Agent to create events by receiving webhooks from any source. |
|
8 |
+ |
|
9 |
+ In order to create events with this agent, make a POST request to: |
|
10 |
+ ``` |
|
11 |
+ https://#{ENV['DOMAIN']}/users/#{user.id}/webhooks/#{id || '<id>'}/:secret |
|
12 |
+ ``` where `:secret` is specified in your options. |
|
13 |
+ |
|
14 |
+ The |
|
15 |
+ |
|
16 |
+ Options: |
|
17 |
+ |
|
18 |
+ * `secret` - A token that the host will provide for authentication. |
|
19 |
+ * `expected_receive_period_in_days` - How often you expect to receive |
|
20 |
+ events this way. Used to determine if the agent is working. |
|
21 |
+ * `payload_path` - JSONPath of the attribute in the POST body to be |
|
22 |
+ used as the Event payload. |
|
23 |
+ MD |
|
24 |
+ end |
|
25 |
+ |
|
26 |
+ event_description do |
|
27 |
+ <<-MD |
|
28 |
+ The event payload is base on the value of the `payload_path` option, |
|
29 |
+ which is set to `#{options['payload_path']}`. |
|
30 |
+ MD |
|
31 |
+ end |
|
32 |
+ |
|
33 |
+ def default_options |
|
34 |
+ { "secret" => "supersecretstring", |
|
35 |
+ "expected_receive_period_in_days" => 1, |
|
36 |
+ "payload_path" => "payload"} |
|
37 |
+ end |
|
38 |
+ |
|
39 |
+ def receive_webhook(params) |
|
40 |
+ secret = params.delete('secret') |
|
41 |
+ return ["Not Authorized", 401] unless secret == options['secret'] |
|
42 |
+ |
|
43 |
+ create_event(:payload => payload_for(params)) |
|
44 |
+ |
|
45 |
+ ['Event Created', 201] |
|
46 |
+ end |
|
47 |
+ |
|
48 |
+ def working? |
|
49 |
+ event_created_within(options['expected_receive_period_in_days']) && !recent_error_logs? |
|
50 |
+ end |
|
51 |
+ |
|
52 |
+ def validate_options |
|
53 |
+ unless options['secret'].present? |
|
54 |
+ errors.add(:base, "Must specify a secret for 'Authenticating' requests") |
|
55 |
+ end |
|
56 |
+ end |
|
57 |
+ |
|
58 |
+ def payload_for(params) |
|
59 |
+ Utils.value_at(params, options['payload_path']) || {} |
|
60 |
+ end |
|
61 |
+ end |
|
62 |
+end |
@@ -15,19 +15,19 @@ module Agents |
||
15 | 15 |
|
16 | 16 |
To tell the Agent how to parse the content, specify `extract` as a hash with keys naming the extractions and values of hashes. |
17 | 17 |
|
18 |
- When parsing HTML or XML, these sub-hashes specify how to extract with a `:css` CSS selector and either `:text => true` or `attr` pointing to an attribute name to grab. An example: |
|
18 |
+ When parsing HTML or XML, these sub-hashes specify how to extract with a `css` CSS selector and either `'text': true` or `attr` pointing to an attribute name to grab. An example: |
|
19 | 19 |
|
20 |
- :extract => { |
|
21 |
- :url => { :css => "#comic img", :attr => "src" }, |
|
22 |
- :title => { :css => "#comic img", :attr => "title" }, |
|
23 |
- :body_text => { :css => "div.main", :text => true } |
|
20 |
+ 'extract': { |
|
21 |
+ 'url': { 'css': "#comic img", 'attr': "src" }, |
|
22 |
+ 'title': { 'css': "#comic img", 'attr': "title" }, |
|
23 |
+ 'body_text': { 'css': "div.main", 'text': true } |
|
24 | 24 |
} |
25 | 25 |
|
26 | 26 |
When parsing JSON, these sub-hashes specify [JSONPaths](http://goessner.net/articles/JsonPath/) to the values that you care about. For example: |
27 | 27 |
|
28 |
- :extract => { |
|
29 |
- :title => { :path => "results.data[*].title" }, |
|
30 |
- :description => { :path => "results.data[*].description" } |
|
28 |
+ 'extract': { |
|
29 |
+ 'title': { 'path': "results.data[*].title" }, |
|
30 |
+ 'description': { 'path': "results.data[*].description" } |
|
31 | 31 |
} |
32 | 32 |
|
33 | 33 |
Note that for all of the formats, whatever you extract MUST have the same number of matches for each extractor. E.g., if you're extracting rows, all extractors must match all rows. For generating CSS selectors, something like [SelectorGadget](http://selectorgadget.com) may be helpful. |
@@ -36,7 +36,7 @@ module Agents |
||
36 | 36 |
MD |
37 | 37 |
|
38 | 38 |
event_description do |
39 |
- "Events will have the fields you specified. Your options look like:\n\n #{Utils.pretty_print options[:extract]}" |
|
39 |
+ "Events will have the fields you specified. Your options look like:\n\n #{Utils.pretty_print options['extract']}" |
|
40 | 40 |
end |
41 | 41 |
|
42 | 42 |
default_schedule "every_12h" |
@@ -44,33 +44,33 @@ module Agents |
||
44 | 44 |
UNIQUENESS_LOOK_BACK = 30 |
45 | 45 |
|
46 | 46 |
def working? |
47 |
- event_created_within(options[:expected_update_period_in_days]) && !recent_error_logs? |
|
47 |
+ event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? |
|
48 | 48 |
end |
49 | 49 |
|
50 | 50 |
def default_options |
51 | 51 |
{ |
52 |
- :expected_update_period_in_days => "2", |
|
53 |
- :url => "http://xkcd.com", |
|
54 |
- :type => "html", |
|
55 |
- :mode => :on_change, |
|
56 |
- :extract => { |
|
57 |
- :url => {:css => "#comic img", :attr => "src"}, |
|
58 |
- :title => {:css => "#comic img", :attr => "title"} |
|
52 |
+ 'expected_update_period_in_days' => "2", |
|
53 |
+ 'url' => "http://xkcd.com", |
|
54 |
+ 'type' => "html", |
|
55 |
+ 'mode' => :on_change, |
|
56 |
+ 'extract' => { |
|
57 |
+ 'url' => {'css' => "#comic img", 'attr' => "src"}, |
|
58 |
+ 'title' => {'css' => "#comic img", 'attr' => "title"} |
|
59 | 59 |
} |
60 | 60 |
} |
61 | 61 |
end |
62 | 62 |
|
63 | 63 |
def validate_options |
64 |
- errors.add(:base, "url and expected_update_period_in_days are required") unless options[:expected_update_period_in_days].present? && options[:url].present? |
|
65 |
- if !options[:extract].present? && extraction_type != "json" |
|
64 |
+ errors.add(:base, "url and expected_update_period_in_days are required") unless options['expected_update_period_in_days'].present? && options['url'].present? |
|
65 |
+ if !options['extract'].present? && extraction_type != "json" |
|
66 | 66 |
errors.add(:base, "extract is required for all types except json") |
67 | 67 |
end |
68 | 68 |
end |
69 | 69 |
|
70 | 70 |
def check |
71 | 71 |
hydra = Typhoeus::Hydra.new |
72 |
- log "Fetching #{options[:url]}" |
|
73 |
- request = Typhoeus::Request.new(options[:url], :followlocation => true) |
|
72 |
+ log "Fetching #{options['url']}" |
|
73 |
+ request = Typhoeus::Request.new(options['url'], :followlocation => true) |
|
74 | 74 |
request.on_failure do |response| |
75 | 75 |
error "Failed: #{response.inspect}" |
76 | 76 |
end |
@@ -85,37 +85,37 @@ module Agents |
||
85 | 85 |
end |
86 | 86 |
else |
87 | 87 |
output = {} |
88 |
- options[:extract].each do |name, extraction_details| |
|
88 |
+ options['extract'].each do |name, extraction_details| |
|
89 | 89 |
result = if extraction_type == "json" |
90 |
- output[name] = Utils.values_at(doc, extraction_details[:path]) |
|
90 |
+ output[name] = Utils.values_at(doc, extraction_details['path']) |
|
91 | 91 |
else |
92 |
- output[name] = doc.css(extraction_details[:css]).map { |node| |
|
93 |
- if extraction_details[:attr] |
|
94 |
- node.attr(extraction_details[:attr]) |
|
95 |
- elsif extraction_details[:text] |
|
92 |
+ output[name] = doc.css(extraction_details['css']).map { |node| |
|
93 |
+ if extraction_details['attr'] |
|
94 |
+ node.attr(extraction_details['attr']) |
|
95 |
+ elsif extraction_details['text'] |
|
96 | 96 |
node.text() |
97 | 97 |
else |
98 |
- error ":attr or :text is required on HTML or XML extraction patterns" |
|
98 |
+ error "'attr' or 'text' is required on HTML or XML extraction patterns" |
|
99 | 99 |
return |
100 | 100 |
end |
101 | 101 |
} |
102 | 102 |
end |
103 |
- log "Extracting #{extraction_type} at #{extraction_details[:path] || extraction_details[:css]}: #{result}" |
|
103 |
+ log "Extracting #{extraction_type} at #{extraction_details['path'] || extraction_details['css']}: #{result}" |
|
104 | 104 |
end |
105 | 105 |
|
106 |
- num_unique_lengths = options[:extract].keys.map { |name| output[name].length }.uniq |
|
106 |
+ num_unique_lengths = options['extract'].keys.map { |name| output[name].length }.uniq |
|
107 | 107 |
|
108 | 108 |
if num_unique_lengths.length != 1 |
109 |
- error "Got an uneven number of matches for #{options[:name]}: #{options[:extract].inspect}" |
|
109 |
+ error "Got an uneven number of matches for #{options['name']}: #{options['extract'].inspect}" |
|
110 | 110 |
return |
111 | 111 |
end |
112 | 112 |
|
113 | 113 |
num_unique_lengths.first.times do |index| |
114 | 114 |
result = {} |
115 |
- options[:extract].keys.each do |name| |
|
115 |
+ options['extract'].keys.each do |name| |
|
116 | 116 |
result[name] = output[name][index] |
117 | 117 |
if name.to_s == 'url' |
118 |
- result[name] = URI.join(options[:url], result[name]).to_s if (result[name] =~ URI::DEFAULT_PARSER.regexp[:ABS_URI]).nil? |
|
118 |
+ result[name] = URI.join(options['url'], result[name]).to_s if (result[name] =~ URI::DEFAULT_PARSER.regexp[:ABS_URI]).nil? |
|
119 | 119 |
end |
120 | 120 |
end |
121 | 121 |
|
@@ -133,22 +133,22 @@ module Agents |
||
133 | 133 |
private |
134 | 134 |
|
135 | 135 |
def store_payload? result |
136 |
- !options[:mode] || options[:mode].to_s == "all" || (options[:mode].to_s == "on_change" && !previous_payloads.include?(result.to_json)) |
|
136 |
+ !options['mode'] || options['mode'].to_s == "all" || (options['mode'].to_s == "on_change" && !previous_payloads.include?(result.to_json)) |
|
137 | 137 |
end |
138 | 138 |
|
139 | 139 |
def previous_payloads |
140 |
- events.order("id desc").limit(UNIQUENESS_LOOK_BACK).pluck(:payload).map(&:to_json) if options[:mode].to_s == "on_change" |
|
140 |
+ events.order("id desc").limit(UNIQUENESS_LOOK_BACK).pluck(:payload).map(&:to_json) if options['mode'].to_s == "on_change" |
|
141 | 141 |
end |
142 | 142 |
|
143 | 143 |
def extract_full_json? |
144 |
- (!options[:extract].present? && extraction_type == "json") |
|
144 |
+ (!options['extract'].present? && extraction_type == "json") |
|
145 | 145 |
end |
146 | 146 |
|
147 | 147 |
def extraction_type |
148 |
- (options[:type] || begin |
|
149 |
- if options[:url] =~ /\.(rss|xml)$/i |
|
148 |
+ (options['type'] || begin |
|
149 |
+ if options['url'] =~ /\.(rss|xml)$/i |
|
150 | 150 |
"xml" |
151 |
- elsif options[:url] =~ /\.json$/i |
|
151 |
+ elsif options['url'] =~ /\.json$/i |
|
152 | 152 |
"json" |
153 | 153 |
else |
154 | 154 |
"html" |
@@ -20,24 +20,24 @@ module Agents |
||
20 | 20 |
MD |
21 | 21 |
|
22 | 22 |
def validate_options |
23 |
- unless options[:uid].present? && |
|
24 |
- options[:expected_update_period_in_days].present? |
|
23 |
+ unless options['uid'].present? && |
|
24 |
+ options['expected_update_period_in_days'].present? |
|
25 | 25 |
errors.add(:base, "expected_update_period_in_days and uid are required") |
26 | 26 |
end |
27 | 27 |
end |
28 | 28 |
|
29 | 29 |
def working? |
30 |
- (event = event_created_within(options[:expected_update_period_in_days])) && event.payload[:success] == true && !recent_error_logs? |
|
30 |
+ event_created_within?(options['expected_update_period_in_days']) && most_recent_event.payload['success'] == true && !recent_error_logs? |
|
31 | 31 |
end |
32 | 32 |
|
33 | 33 |
def default_options |
34 | 34 |
{ |
35 |
- :uid => "", |
|
36 |
- :access_token => "---", |
|
37 |
- :app_key => "---", |
|
38 |
- :app_secret => "---", |
|
39 |
- :expected_update_period_in_days => "10", |
|
40 |
- :message_path => "text" |
|
35 |
+ 'uid' => "", |
|
36 |
+ 'access_token' => "---", |
|
37 |
+ 'app_key' => "---", |
|
38 |
+ 'app_secret' => "---", |
|
39 |
+ 'expected_update_period_in_days' => "10", |
|
40 |
+ 'message_path' => "text" |
|
41 | 41 |
} |
42 | 42 |
end |
43 | 43 |
|
@@ -47,25 +47,25 @@ module Agents |
||
47 | 47 |
incoming_events = incoming_events.first(20) |
48 | 48 |
end |
49 | 49 |
incoming_events.each do |event| |
50 |
- tweet_text = Utils.value_at(event.payload, options[:message_path]) |
|
50 |
+ tweet_text = Utils.value_at(event.payload, options['message_path']) |
|
51 | 51 |
if event.agent.type == "Agents::TwitterUserAgent" |
52 | 52 |
tweet_text = unwrap_tco_urls(tweet_text, event.payload) |
53 | 53 |
end |
54 | 54 |
begin |
55 | 55 |
publish_tweet tweet_text |
56 | 56 |
create_event :payload => { |
57 |
- :success => true, |
|
58 |
- :published_tweet => tweet_text, |
|
59 |
- :agent_id => event.agent_id, |
|
60 |
- :event_id => event.id |
|
57 |
+ 'success' => true, |
|
58 |
+ 'published_tweet' => tweet_text, |
|
59 |
+ 'agent_id' => event.agent_id, |
|
60 |
+ 'event_id' => event.id |
|
61 | 61 |
} |
62 | 62 |
rescue OAuth2::Error => e |
63 | 63 |
create_event :payload => { |
64 |
- :success => false, |
|
65 |
- :error => e.message, |
|
66 |
- :failed_tweet => tweet_text, |
|
67 |
- :agent_id => event.agent_id, |
|
68 |
- :event_id => event.id |
|
64 |
+ 'success' => false, |
|
65 |
+ 'error' => e.message, |
|
66 |
+ 'failed_tweet' => tweet_text, |
|
67 |
+ 'agent_id' => event.agent_id, |
|
68 |
+ 'event_id' => event.id |
|
69 | 69 |
} |
70 | 70 |
end |
71 | 71 |
end |
@@ -70,29 +70,29 @@ module Agents |
||
70 | 70 |
default_schedule "every_1h" |
71 | 71 |
|
72 | 72 |
def validate_options |
73 |
- unless options[:uid].present? && |
|
74 |
- options[:expected_update_period_in_days].present? |
|
73 |
+ unless options['uid'].present? && |
|
74 |
+ options['expected_update_period_in_days'].present? |
|
75 | 75 |
errors.add(:base, "expected_update_period_in_days and uid are required") |
76 | 76 |
end |
77 | 77 |
end |
78 | 78 |
|
79 | 79 |
def working? |
80 |
- event_created_within(options[:expected_update_period_in_days]) && !recent_error_logs? |
|
80 |
+ event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? |
|
81 | 81 |
end |
82 | 82 |
|
83 | 83 |
def default_options |
84 | 84 |
{ |
85 |
- :uid => "", |
|
86 |
- :access_token => "---", |
|
87 |
- :app_key => "---", |
|
88 |
- :app_secret => "---", |
|
89 |
- :expected_update_period_in_days => "2" |
|
85 |
+ 'uid' => "", |
|
86 |
+ 'access_token' => "---", |
|
87 |
+ 'app_key' => "---", |
|
88 |
+ 'app_secret' => "---", |
|
89 |
+ 'expected_update_period_in_days' => "2" |
|
90 | 90 |
} |
91 | 91 |
end |
92 | 92 |
|
93 | 93 |
def check |
94 |
- since_id = memory[:since_id] || nil |
|
95 |
- opts = {:uid => options[:uid].to_i} |
|
94 |
+ since_id = memory['since_id'] || nil |
|
95 |
+ opts = {:uid => options['uid'].to_i} |
|
96 | 96 |
opts.merge! :since_id => since_id unless since_id.nil? |
97 | 97 |
|
98 | 98 |
# http://open.weibo.com/wiki/2/statuses/user_timeline/en |
@@ -101,7 +101,7 @@ module Agents |
||
101 | 101 |
|
102 | 102 |
|
103 | 103 |
resp[:statuses].each do |status| |
104 |
- memory[:since_id] = status.id if !memory[:since_id] || (status.id > memory[:since_id]) |
|
104 |
+ memory['since_id'] = status.id if !memory['since_id'] || (status.id > memory['since_id']) |
|
105 | 105 |
|
106 | 106 |
create_event :payload => status.as_json |
107 | 107 |
end |
@@ -1,23 +1,21 @@ |
||
1 |
+require 'json_serialized_field' |
|
2 |
+ |
|
1 | 3 |
class Event < ActiveRecord::Base |
4 |
+ include JSONSerializedField |
|
5 |
+ |
|
2 | 6 |
attr_accessible :lat, :lng, :payload, :user_id, :user, :expires_at |
3 | 7 |
|
4 | 8 |
acts_as_mappable |
5 | 9 |
|
6 |
- serialize :payload |
|
10 |
+ json_serialize :payload |
|
7 | 11 |
|
8 | 12 |
belongs_to :user |
9 |
- belongs_to :agent, :counter_cache => true |
|
10 |
- |
|
11 |
- before_save :symbolize_payload |
|
13 |
+ belongs_to :agent, :counter_cache => true, :touch => :last_event_at |
|
12 | 14 |
|
13 | 15 |
scope :recent, lambda { |timespan = 12.hours.ago| |
14 | 16 |
where("events.created_at > ?", timespan) |
15 | 17 |
} |
16 | 18 |
|
17 |
- def symbolize_payload |
|
18 |
- self.payload = payload.recursively_symbolize_keys if payload.is_a?(Hash) |
|
19 |
- end |
|
20 |
- |
|
21 | 19 |
def reemit! |
22 | 20 |
agent.create_event :payload => payload, :lat => lat, :lng => lng |
23 | 21 |
end |
@@ -11,7 +11,7 @@ |
||
11 | 11 |
<li class='disabled'><a><i class='icon-picture'></i> Summary</a></li> |
12 | 12 |
<li class='active'><a href="#details" data-toggle="tab"><i class='icon-indent-left'></i> Details</a></li> |
13 | 13 |
<% end %> |
14 |
- <li><a href="#logs" data-toggle="tab" data-agent-id="<%= @agent.id %>"><i class='icon-list-alt'></i> Logs</a></li> |
|
14 |
+ <li><a href="#logs" data-toggle="tab" data-agent-id="<%= @agent.id %>" class='<%= @agent.recent_error_logs? ? 'recent-errors' : '' %>'><i class='icon-list-alt'></i> Logs</a></li> |
|
15 | 15 |
|
16 | 16 |
<% if @agent.can_create_events? && @agent.events.count > 0 %> |
17 | 17 |
<li><%= link_to '<i class="icon-random"></i> Events'.html_safe, events_path(:agent => @agent.to_param) %></li> |
@@ -36,7 +36,7 @@ |
||
36 | 36 |
<script> |
37 | 37 |
var agentPaths = {}; |
38 | 38 |
<% if current_user -%> |
39 |
- var myAgents = <%= Utils.jsonify(current_user.agents.inject({}) {|m, a| m[a.name] = agent_path(a) unless a.new_record?; m }) %>; |
|
39 |
+ var myAgents = <%= Utils.jsonify(current_user.agents.select([:name, :id, :schedule]).inject({}) {|m, a| m[a.name] = agent_path(a); m }) %>; |
|
40 | 40 |
$.extend(agentPaths, myAgents); |
41 | 41 |
<% end -%> |
42 | 42 |
agentPaths["All Agents Index"] = <%= Utils.jsonify agents_path %>; |
@@ -1,4 +1,4 @@ |
||
1 |
-Delayed::Worker.destroy_failed_jobs = false |
|
1 |
+Delayed::Worker.destroy_failed_jobs = true |
|
2 | 2 |
Delayed::Worker.max_attempts = 5 |
3 | 3 |
Delayed::Worker.max_run_time = 20.minutes |
4 | 4 |
Delayed::Worker.default_priority = 10 |
@@ -1,7 +0,0 @@ |
||
1 |
-require 'utils' |
|
2 |
- |
|
3 |
-class Hash |
|
4 |
- def recursively_symbolize_keys |
|
5 |
- Utils.recursively_symbolize_keys self |
|
6 |
- end |
|
7 |
-end |
@@ -0,0 +1,67 @@ |
||
1 |
+class SwitchToJsonSerialization < ActiveRecord::Migration |
|
2 |
+ FIELDS = { |
|
3 |
+ :agents => [:options, :memory], |
|
4 |
+ :events => [:payload] |
|
5 |
+ } |
|
6 |
+ |
|
7 |
+ def up |
|
8 |
+ if data_exists? |
|
9 |
+ puts "This migration will update tables to use UTF-8 encoding and will update Agent and Event storage from YAML to JSON." |
|
10 |
+ puts "It should work, but please make a backup before proceeding!" |
|
11 |
+ print "Continue? (y/n) " |
|
12 |
+ STDOUT.flush |
|
13 |
+ exit unless STDIN.gets =~ /^y/i |
|
14 |
+ |
|
15 |
+ set_to_utf8 |
|
16 |
+ translate YAML, JSON |
|
17 |
+ end |
|
18 |
+ end |
|
19 |
+ |
|
20 |
+ def down |
|
21 |
+ if data_exists? |
|
22 |
+ translate JSON, YAML |
|
23 |
+ end |
|
24 |
+ end |
|
25 |
+ |
|
26 |
+ def set_to_utf8 |
|
27 |
+ if mysql? |
|
28 |
+ %w[agent_logs agents delayed_jobs events links users].each do |table_name| |
|
29 |
+ quoted_table_name = ActiveRecord::Base.connection.quote_table_name(table_name) |
|
30 |
+ execute "ALTER TABLE #{quoted_table_name} CONVERT TO CHARACTER SET utf8" |
|
31 |
+ end |
|
32 |
+ end |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+ def mysql? |
|
36 |
+ ActiveRecord::Base.connection.adapter_name =~ /mysql/i |
|
37 |
+ end |
|
38 |
+ |
|
39 |
+ def data_exists? |
|
40 |
+ events = ActiveRecord::Base.connection.select_rows("SELECT count(*) FROM #{ActiveRecord::Base.connection.quote_table_name("events")}").first.first |
|
41 |
+ agents = ActiveRecord::Base.connection.select_rows("SELECT count(*) FROM #{ActiveRecord::Base.connection.quote_table_name("agents")}").first.first |
|
42 |
+ agents + events > 0 |
|
43 |
+ end |
|
44 |
+ |
|
45 |
+ def translate(from, to) |
|
46 |
+ FIELDS.each do |table, fields| |
|
47 |
+ quoted_table_name = ActiveRecord::Base.connection.quote_table_name(table) |
|
48 |
+ fields = fields.map { |f| ActiveRecord::Base.connection.quote_column_name(f) } |
|
49 |
+ |
|
50 |
+ rows = ActiveRecord::Base.connection.select_rows("SELECT id, #{fields.join(", ")} FROM #{quoted_table_name}") |
|
51 |
+ rows.each do |row| |
|
52 |
+ id, *field_data = row |
|
53 |
+ |
|
54 |
+ yaml_fields = field_data.map { |f| from.load(f) }.map { |f| to.dump(f) } |
|
55 |
+ |
|
56 |
+ yaml_fields.map! {|f| f.encode('utf-8', 'binary', invalid: :replace, undef: :replace, replace: '??') } |
|
57 |
+ |
|
58 |
+ update_sql = "UPDATE #{quoted_table_name} SET #{fields.map {|f| "#{f}=?"}.join(", ")} WHERE id = ?" |
|
59 |
+ |
|
60 |
+ sanitized_update_sql = ActiveRecord::Base.send :sanitize_sql_array, [update_sql, *yaml_fields, id] |
|
61 |
+ |
|
62 |
+ ActiveRecord::Base.connection.execute sanitized_update_sql |
|
63 |
+ end |
|
64 |
+ end |
|
65 |
+ |
|
66 |
+ end |
|
67 |
+end |
@@ -0,0 +1,14 @@ |
||
1 |
+class AddCachedDatesToAgent < ActiveRecord::Migration |
|
2 |
+ def up |
|
3 |
+ add_column :agents, :last_event_at, :datetime |
|
4 |
+ execute "UPDATE agents SET last_event_at = (SELECT created_at FROM events WHERE events.agent_id = agents.id ORDER BY id DESC LIMIT 1)" |
|
5 |
+ |
|
6 |
+ add_column :agents, :last_error_log_at, :datetime |
|
7 |
+ execute "UPDATE agents SET last_error_log_at = (SELECT created_at FROM agent_logs WHERE agent_logs.agent_id = agents.id AND agent_logs.level >= 4 ORDER BY id DESC LIMIT 1)" |
|
8 |
+ end |
|
9 |
+ |
|
10 |
+ def down |
|
11 |
+ remove_column :agents, :last_event_at |
|
12 |
+ remove_column :agents, :last_error_log_at |
|
13 |
+ end |
|
14 |
+end |
@@ -55,6 +55,6 @@ Backup::Model.new(:huginn_backup, 'The Huginn backup configuration') do |
||
55 | 55 |
mail.password = 'password' |
56 | 56 |
mail.port = 587 |
57 | 57 |
mail.authentication = "plain" |
58 |
- mail.enable_starttls_auto = true |
|
58 |
+ mail.encryption = :starttls |
|
59 | 59 |
end |
60 | 60 |
end |
@@ -1,5 +1,6 @@ |
||
1 | 1 |
require 'yaml' |
2 | 2 |
require 'pathname' |
3 |
+require 'dotenv' |
|
3 | 4 |
|
4 | 5 |
# Edited by Andrew Cantino. Based on: https://gist.github.com/339471 |
5 | 6 |
|
@@ -89,13 +90,42 @@ namespace :sync do |
||
89 | 90 |
end |
90 | 91 |
end |
91 | 92 |
|
93 |
+ # Used by database_config and remote_database_config to parse database configs that depend on .env files. Depends on the dotenv-rails gem. |
|
94 |
+ class EnvLoader < Dotenv::Environment |
|
95 |
+ def initialize(data) |
|
96 |
+ @data = data |
|
97 |
+ load |
|
98 |
+ end |
|
99 |
+ |
|
100 |
+ def with_loaded_env |
|
101 |
+ begin |
|
102 |
+ saved_env = ENV.to_hash.dup |
|
103 |
+ ENV.update(self) |
|
104 |
+ yield |
|
105 |
+ ensure |
|
106 |
+ ENV.replace(saved_env) |
|
107 |
+ end |
|
108 |
+ end |
|
109 |
+ |
|
110 |
+ def read |
|
111 |
+ @data.split("\n") |
|
112 |
+ end |
|
113 |
+ end |
|
114 |
+ |
|
92 | 115 |
# |
93 | 116 |
# Reads the database credentials from the local config/database.yml file |
94 | 117 |
# +db+ the name of the environment to get the credentials for |
95 | 118 |
# Returns username, password, database |
96 | 119 |
# |
97 | 120 |
def database_config(db) |
98 |
- database = YAML::load_file('config/database.yml') |
|
121 |
+ local_config = File.read('config/database.yml') |
|
122 |
+ local_env = File.read('.env') |
|
123 |
+ |
|
124 |
+ database = nil |
|
125 |
+ EnvLoader.new(local_env).with_loaded_env do |
|
126 |
+ database = YAML::load(ERB.new(local_config).result) |
|
127 |
+ end |
|
128 |
+ |
|
99 | 129 |
return database["#{db}"]['username'], database["#{db}"]['password'], database["#{db}"]['database'], database["#{db}"]['host'] |
100 | 130 |
end |
101 | 131 |
|
@@ -106,7 +136,13 @@ namespace :sync do |
||
106 | 136 |
# |
107 | 137 |
def remote_database_config(db) |
108 | 138 |
remote_config = capture("cat #{current_path}/config/database.yml") |
109 |
- database = YAML::load(remote_config) |
|
139 |
+ remote_env = capture("cat #{current_path}/.env") |
|
140 |
+ |
|
141 |
+ database = nil |
|
142 |
+ EnvLoader.new(remote_env).with_loaded_env do |
|
143 |
+ database = YAML::load(ERB.new(remote_config).result) |
|
144 |
+ end |
|
145 |
+ |
|
110 | 146 |
return database["#{db}"]['username'], database["#{db}"]['password'], database["#{db}"]['database'], database["#{db}"]['host'] |
111 | 147 |
end |
112 | 148 |
|
@@ -0,0 +1,42 @@ |
||
1 |
+require 'json_with_indifferent_access' |
|
2 |
+ |
|
3 |
+module JSONSerializedField |
|
4 |
+ extend ActiveSupport::Concern |
|
5 |
+ |
|
6 |
+ module ClassMethods |
|
7 |
+ def json_serialize(*fields) |
|
8 |
+ fields.each do |field| |
|
9 |
+ class_eval <<-CODE |
|
10 |
+ serialize :#{field}, JSONWithIndifferentAccess |
|
11 |
+ |
|
12 |
+ validate :#{field}_has_no_errors |
|
13 |
+ |
|
14 |
+ def #{field}=(input) |
|
15 |
+ @#{field}_assignment_error = false |
|
16 |
+ case input |
|
17 |
+ when String |
|
18 |
+ if input.strip.length == 0 |
|
19 |
+ self[:#{field}] = ActiveSupport::HashWithIndifferentAccess.new |
|
20 |
+ else |
|
21 |
+ json = JSON.parse(input) rescue nil |
|
22 |
+ if json |
|
23 |
+ self[:#{field}] = ActiveSupport::HashWithIndifferentAccess.new(json) |
|
24 |
+ else |
|
25 |
+ @#{field}_assignment_error = "was assigned invalid JSON" |
|
26 |
+ end |
|
27 |
+ end |
|
28 |
+ when Hash |
|
29 |
+ self[:#{field}] = ActiveSupport::HashWithIndifferentAccess.new(input) |
|
30 |
+ else |
|
31 |
+ @#{field}_assignment_error = "cannot be set to an instance of \#{input.class}" |
|
32 |
+ end |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+ def #{field}_has_no_errors |
|
36 |
+ errors.add(:#{field}, @#{field}_assignment_error) if @#{field}_assignment_error |
|
37 |
+ end |
|
38 |
+ CODE |
|
39 |
+ end |
|
40 |
+ end |
|
41 |
+ end |
|
42 |
+end |
@@ -0,0 +1,9 @@ |
||
1 |
+class JSONWithIndifferentAccess |
|
2 |
+ def self.load(json) |
|
3 |
+ ActiveSupport::HashWithIndifferentAccess.new(JSON.parse(json || '{}')) |
|
4 |
+ end |
|
5 |
+ |
|
6 |
+ def self.dump(hash) |
|
7 |
+ JSON.dump(hash) |
|
8 |
+ end |
|
9 |
+end |
@@ -1,43 +0,0 @@ |
||
1 |
-module SerializeAndSymbolize |
|
2 |
- extend ActiveSupport::Concern |
|
3 |
- |
|
4 |
- module ClassMethods |
|
5 |
- def serialize_and_symbolize(*column_names) |
|
6 |
- column_names.flatten.uniq.compact.map(&:to_sym).each do |column_name| |
|
7 |
- setup_name = "setup_#{column_name}".to_sym |
|
8 |
- symbolize_name = "symbolize_#{column_name}".to_sym |
|
9 |
- validate_name = "validate_#{column_name}".to_sym |
|
10 |
- |
|
11 |
- serialize column_name |
|
12 |
- after_initialize setup_name |
|
13 |
- before_validation symbolize_name |
|
14 |
- before_save symbolize_name |
|
15 |
- validate validate_name |
|
16 |
- |
|
17 |
- class_eval <<-RUBY |
|
18 |
- def #{setup_name} |
|
19 |
- self[:#{column_name}] ||= {} |
|
20 |
- end |
|
21 |
- |
|
22 |
- def #{validate_name} |
|
23 |
- # Implement me in your subclass. |
|
24 |
- end |
|
25 |
- |
|
26 |
- def #{symbolize_name} |
|
27 |
- self.#{column_name} = self[:#{column_name}] |
|
28 |
- end |
|
29 |
- |
|
30 |
- def #{column_name}=(data) |
|
31 |
- if data.is_a?(String) |
|
32 |
- self[:#{column_name}] = JSON.parse(data).recursively_symbolize_keys rescue {} |
|
33 |
- elsif data.is_a?(Hash) |
|
34 |
- self[:#{column_name}] = data.recursively_symbolize_keys |
|
35 |
- else |
|
36 |
- self[:#{column_name}] = data |
|
37 |
- end |
|
38 |
- end |
|
39 |
- RUBY |
|
40 |
- end |
|
41 |
- end |
|
42 |
- end |
|
43 |
-end |
@@ -21,17 +21,6 @@ module Utils |
||
21 | 21 |
end |
22 | 22 |
end |
23 | 23 |
|
24 |
- def self.recursively_symbolize_keys(object) |
|
25 |
- case object |
|
26 |
- when Hash |
|
27 |
- object.inject({}) {|memo, (k, v)| memo[String === k ? k.to_sym : k] = recursively_symbolize_keys(v); memo } |
|
28 |
- when Array |
|
29 |
- object.map { |item| recursively_symbolize_keys item } |
|
30 |
- else |
|
31 |
- object |
|
32 |
- end |
|
33 |
- end |
|
34 |
- |
|
35 | 24 |
def self.interpolate_jsonpaths(value, data) |
36 | 25 |
value.gsub(/<[^>]+>/).each { |jsonpath| |
37 | 26 |
Utils.values_at(data, jsonpath[1..-2]).first.to_s |
@@ -5,7 +5,7 @@ describe AgentsController do |
||
5 | 5 |
{ |
6 | 6 |
:type => "Agents::WebsiteAgent", |
7 | 7 |
:name => "Something", |
8 |
- :options => agents(:bob_website_agent).options.to_json, |
|
8 |
+ :options => agents(:bob_website_agent).options, |
|
9 | 9 |
:source_ids => [agents(:bob_weather_agent).id, ""] |
10 | 10 |
}.merge(options) |
11 | 11 |
end |
@@ -23,7 +23,7 @@ describe AgentsController do |
||
23 | 23 |
sign_in users(:bob) |
24 | 24 |
post :handle_details_post, :id => agents(:bob_manual_event_agent).to_param, :payload => { :foo => "bar" } |
25 | 25 |
JSON.parse(response.body).should == { "success" => true } |
26 |
- agents(:bob_manual_event_agent).events.last.payload.should == { :foo => "bar" } |
|
26 |
+ agents(:bob_manual_event_agent).events.last.payload.should == { 'foo' => "bar" } |
|
27 | 27 |
end |
28 | 28 |
|
29 | 29 |
it "can only be accessed by the Agent's owner" do |
@@ -19,12 +19,14 @@ describe LogsController do |
||
19 | 19 |
|
20 | 20 |
describe "DELETE clear" do |
21 | 21 |
it "deletes all logs for a specific Agent" do |
22 |
+ agents(:bob_weather_agent).last_error_log_at = 2.hours.ago |
|
22 | 23 |
sign_in users(:bob) |
23 | 24 |
lambda { |
24 | 25 |
delete :clear, :agent_id => agents(:bob_weather_agent).id |
25 | 26 |
}.should change { AgentLog.count }.by(-1 * agents(:bob_weather_agent).logs.count) |
26 | 27 |
assigns(:logs).length.should == 0 |
27 |
- agents(:bob_weather_agent).logs.count.should == 0 |
|
28 |
+ agents(:bob_weather_agent).reload.logs.count.should == 0 |
|
29 |
+ agents(:bob_weather_agent).last_error_log_at.should be_nil |
|
28 | 30 |
end |
29 | 31 |
|
30 | 32 |
it "only deletes logs for an Agent owned by the current user" do |
@@ -8,7 +8,7 @@ describe UserLocationUpdatesController do |
||
8 | 8 |
|
9 | 9 |
it "should create events without requiring login" do |
10 | 10 |
post :create, :user_id => users(:bob).to_param, :secret => "my_secret", :longitude => 123, :latitude => 45, :something => "else" |
11 |
- @agent.events.last.payload.should == { :longitude => "123", :latitude => "45", :something => "else" } |
|
11 |
+ @agent.events.last.payload.should == { 'longitude' => "123", 'latitude' => "45", 'something' => "else" } |
|
12 | 12 |
@agent.events.last.lat.should == 45 |
13 | 13 |
@agent.events.last.lng.should == 123 |
14 | 14 |
end |
@@ -18,7 +18,7 @@ describe UserLocationUpdatesController do |
||
18 | 18 |
@jane_agent.save! |
19 | 19 |
|
20 | 20 |
post :create, :user_id => users(:bob).to_param, :secret => "my_secret", :longitude => 123, :latitude => 45, :something => "else" |
21 |
- @agent.events.last.payload.should == { :longitude => "123", :latitude => "45", :something => "else" } |
|
21 |
+ @agent.events.last.payload.should == { 'longitude' => "123", 'latitude' => "45", 'something' => "else" } |
|
22 | 22 |
@jane_agent.events.should be_empty |
23 | 23 |
end |
24 | 24 |
|
@@ -33,7 +33,7 @@ describe UserLocationUpdatesController do |
||
33 | 33 |
|
34 | 34 |
lambda { |
35 | 35 |
post :create, :user_id => users(:bob).to_param, :secret => "my_secret2", :longitude => 123, :latitude => 45, :something => "else" |
36 |
- @agent2.events.last.payload.should == { :longitude => "123", :latitude => "45", :something => "else" } |
|
36 |
+ @agent2.events.last.payload.should == { 'longitude' => "123", 'latitude' => "45", 'something' => "else" } |
|
37 | 37 |
}.should_not change { @agent.events.count } |
38 | 38 |
end |
39 | 39 |
end |
@@ -32,12 +32,12 @@ describe WebhooksController do |
||
32 | 32 |
|
33 | 33 |
it "should call receive_webhook" do |
34 | 34 |
post :create, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5" |
35 |
- @agent.reload.memory[:webhook_values].should == { :key => "value", :another_key => "5" } |
|
35 |
+ @agent.reload.memory[:webhook_values].should == { 'key' => "value", 'another_key' => "5" } |
|
36 | 36 |
response.body.should == "success" |
37 | 37 |
response.should be_success |
38 | 38 |
|
39 | 39 |
post :create, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "not_my_secret", :no => "go" |
40 |
- @agent.reload.memory[:webhook_values].should_not == { :no => "go" } |
|
40 |
+ @agent.reload.memory[:webhook_values].should_not == { 'no' => "go" } |
|
41 | 41 |
response.body.should == "failure" |
42 | 42 |
response.should be_missing |
43 | 43 |
end |
@@ -11,7 +11,7 @@ jane_website_agent: |
||
11 | 11 |
:title => {:css => "item title", :text => true}, |
12 | 12 |
:url => {:css => "item link", :text => true} |
13 | 13 |
} |
14 |
- }.to_yaml.inspect %> |
|
14 |
+ }.to_json.inspect %> |
|
15 | 15 |
|
16 | 16 |
bob_website_agent: |
17 | 17 |
type: Agents::WebsiteAgent |
@@ -26,7 +26,7 @@ bob_website_agent: |
||
26 | 26 |
:url => {:css => "#comic img", :attr => "src"}, |
27 | 27 |
:title => {:css => "#comic img", :attr => "title"} |
28 | 28 |
} |
29 |
- }.to_yaml.inspect %> |
|
29 |
+ }.to_json.inspect %> |
|
30 | 30 |
|
31 | 31 |
bob_weather_agent: |
32 | 32 |
type: Agents::WeatherAgent |
@@ -39,7 +39,7 @@ bob_weather_agent: |
||
39 | 39 |
:lat => 37.779329, |
40 | 40 |
:lng => -122.41915, |
41 | 41 |
:api_key => 'test' |
42 |
- }.to_yaml.inspect %> |
|
42 |
+ }.to_json.inspect %> |
|
43 | 43 |
|
44 | 44 |
jane_weather_agent: |
45 | 45 |
type: Agents::WeatherAgent |
@@ -52,7 +52,7 @@ jane_weather_agent: |
||
52 | 52 |
:lat => 37.779329, |
53 | 53 |
:lng => -122.41915, |
54 | 54 |
:api_key => 'test' |
55 |
- }.to_yaml.inspect %> |
|
55 |
+ }.to_json.inspect %> |
|
56 | 56 |
|
57 | 57 |
jane_rain_notifier_agent: |
58 | 58 |
type: Agents::TriggerAgent |
@@ -66,7 +66,7 @@ jane_rain_notifier_agent: |
||
66 | 66 |
:path => "conditions" |
67 | 67 |
}], |
68 | 68 |
:message => "Just so you know, it looks like '<conditions>' tomorrow in <location>" |
69 |
- }.to_yaml.inspect %> |
|
69 |
+ }.to_json.inspect %> |
|
70 | 70 |
|
71 | 71 |
bob_rain_notifier_agent: |
72 | 72 |
type: Agents::TriggerAgent |
@@ -80,7 +80,7 @@ bob_rain_notifier_agent: |
||
80 | 80 |
:path => "conditions" |
81 | 81 |
}], |
82 | 82 |
:message => "Just so you know, it looks like '<conditions>' tomorrow in <location>" |
83 |
- }.to_yaml.inspect %> |
|
83 |
+ }.to_json.inspect %> |
|
84 | 84 |
|
85 | 85 |
bob_twitter_user_agent: |
86 | 86 |
type: Agents::TwitterUserAgent |
@@ -93,7 +93,7 @@ bob_twitter_user_agent: |
||
93 | 93 |
:consumer_secret => "---", |
94 | 94 |
:oauth_token => "---", |
95 | 95 |
:oauth_token_secret => "---" |
96 |
- }.to_yaml.inspect %> |
|
96 |
+ }.to_json.inspect %> |
|
97 | 97 |
|
98 | 98 |
bob_manual_event_agent: |
99 | 99 |
type: Agents::ManualEventAgent |
@@ -1,9 +1,9 @@ |
||
1 | 1 |
bob_website_agent_event: |
2 | 2 |
user: bob |
3 | 3 |
agent: bob_website_agent |
4 |
- payload: <%= [{ :title => "foo", :url => "http://foo.com" }].to_yaml.inspect %> |
|
4 |
+ payload: <%= [{ :title => "foo", :url => "http://foo.com" }].to_json.inspect %> |
|
5 | 5 |
|
6 | 6 |
jane_website_agent_event: |
7 | 7 |
user: jane |
8 | 8 |
agent: jane_website_agent |
9 |
- payload: <%= [{ :title => "foo", :url => "http://foo.com" }].to_yaml.inspect %> |
|
9 |
+ payload: <%= [{ :title => "foo", :url => "http://foo.com" }].to_json.inspect %> |
@@ -67,6 +67,14 @@ describe AgentLog do |
||
67 | 67 |
agents(:jane_website_agent).logs.order("agent_logs.id desc").first.message.should == "message 6" |
68 | 68 |
agents(:jane_website_agent).logs.order("agent_logs.id desc").last.message.should == "message 3" |
69 | 69 |
end |
70 |
+ |
|
71 |
+ it "updates Agents' last_error_log_at when an error is logged" do |
|
72 |
+ AgentLog.log_for_agent(agents(:jane_website_agent), "some message", :level => 3, :outbound_event => events(:jane_website_agent_event)) |
|
73 |
+ agents(:jane_website_agent).reload.last_error_log_at.should be_nil |
|
74 |
+ |
|
75 |
+ AgentLog.log_for_agent(agents(:jane_website_agent), "some message", :level => 4, :outbound_event => events(:jane_website_agent_event)) |
|
76 |
+ agents(:jane_website_agent).reload.last_error_log_at.to_i.should be_within(2).of(Time.now.to_i) |
|
77 |
+ end |
|
70 | 78 |
end |
71 | 79 |
|
72 | 80 |
describe "#log_length" do |
@@ -261,9 +261,51 @@ describe Agent do |
||
261 | 261 |
it "makes memory symbol-indifferent before validating" do |
262 | 262 |
agent = Agents::SomethingSource.new(:name => "something") |
263 | 263 |
agent.user = users(:bob) |
264 |
- agent.memory["bad"] = :hello |
|
264 |
+ agent.memory["bad"] = 2 |
|
265 | 265 |
agent.save |
266 |
- agent.memory[:bad].should == :hello |
|
266 |
+ agent.memory[:bad].should == 2 |
|
267 |
+ end |
|
268 |
+ |
|
269 |
+ it "should work when assigned a hash or JSON string" do |
|
270 |
+ agent = Agents::SomethingSource.new(:name => "something") |
|
271 |
+ agent.memory = {} |
|
272 |
+ agent.memory.should == {} |
|
273 |
+ agent.memory["foo"].should be_nil |
|
274 |
+ |
|
275 |
+ agent.memory = "" |
|
276 |
+ agent.memory["foo"].should be_nil |
|
277 |
+ agent.memory.should == {} |
|
278 |
+ |
|
279 |
+ agent.memory = '{"hi": "there"}' |
|
280 |
+ agent.memory.should == { "hi" => "there" } |
|
281 |
+ |
|
282 |
+ agent.memory = '{invalid}' |
|
283 |
+ agent.memory.should == { "hi" => "there" } |
|
284 |
+ agent.should have(1).errors_on(:memory) |
|
285 |
+ |
|
286 |
+ agent.memory = "{}" |
|
287 |
+ agent.memory["foo"].should be_nil |
|
288 |
+ agent.memory.should == {} |
|
289 |
+ agent.should have(0).errors_on(:memory) |
|
290 |
+ |
|
291 |
+ agent.options = "{}" |
|
292 |
+ agent.options["foo"].should be_nil |
|
293 |
+ agent.options.should == {} |
|
294 |
+ agent.should have(0).errors_on(:options) |
|
295 |
+ |
|
296 |
+ agent.options = '{"hi": 2}' |
|
297 |
+ agent.options["hi"].should == 2 |
|
298 |
+ agent.should have(0).errors_on(:options) |
|
299 |
+ |
|
300 |
+ agent.options = '{"hi": wut}' |
|
301 |
+ agent.options["hi"].should == 2 |
|
302 |
+ agent.should have(1).errors_on(:options) |
|
303 |
+ agent.errors_on(:options).should include("was assigned invalid JSON") |
|
304 |
+ |
|
305 |
+ agent.options = 5 |
|
306 |
+ agent.options["hi"].should == 2 |
|
307 |
+ agent.should have(1).errors_on(:options) |
|
308 |
+ agent.errors_on(:options).should include("cannot be set to an instance of Fixnum") |
|
267 | 309 |
end |
268 | 310 |
|
269 | 311 |
it "should not allow agents owned by other people" do |
@@ -357,6 +399,32 @@ describe Agent do |
||
357 | 399 |
|
358 | 400 |
end |
359 | 401 |
|
402 |
+ describe "recent_error_logs?" do |
|
403 |
+ it "returns true if last_error_log_at is near last_event_at" do |
|
404 |
+ agent = Agent.new |
|
405 |
+ |
|
406 |
+ agent.last_error_log_at = 10.minutes.ago |
|
407 |
+ agent.last_event_at = 10.minutes.ago |
|
408 |
+ agent.recent_error_logs?.should be_true |
|
409 |
+ |
|
410 |
+ agent.last_error_log_at = 11.minutes.ago |
|
411 |
+ agent.last_event_at = 10.minutes.ago |
|
412 |
+ agent.recent_error_logs?.should be_true |
|
413 |
+ |
|
414 |
+ agent.last_error_log_at = 5.minutes.ago |
|
415 |
+ agent.last_event_at = 10.minutes.ago |
|
416 |
+ agent.recent_error_logs?.should be_true |
|
417 |
+ |
|
418 |
+ agent.last_error_log_at = 15.minutes.ago |
|
419 |
+ agent.last_event_at = 10.minutes.ago |
|
420 |
+ agent.recent_error_logs?.should be_false |
|
421 |
+ |
|
422 |
+ agent.last_error_log_at = 2.days.ago |
|
423 |
+ agent.last_event_at = 10.minutes.ago |
|
424 |
+ agent.recent_error_logs?.should be_false |
|
425 |
+ end |
|
426 |
+ end |
|
427 |
+ |
|
360 | 428 |
describe "scopes" do |
361 | 429 |
describe "of_type" do |
362 | 430 |
it "should accept classes" do |
@@ -19,16 +19,16 @@ describe Agents::DigestEmailAgent do |
||
19 | 19 |
it "queues any payloads it receives" do |
20 | 20 |
event1 = Event.new |
21 | 21 |
event1.agent = agents(:bob_rain_notifier_agent) |
22 |
- event1.payload = "Something you should know about" |
|
22 |
+ event1.payload = { :data => "Something you should know about" } |
|
23 | 23 |
event1.save! |
24 | 24 |
|
25 | 25 |
event2 = Event.new |
26 | 26 |
event2.agent = agents(:bob_weather_agent) |
27 |
- event2.payload = "Something else you should know about" |
|
27 |
+ event2.payload = { :data => "Something else you should know about" } |
|
28 | 28 |
event2.save! |
29 | 29 |
|
30 | 30 |
Agents::DigestEmailAgent.async_receive(@checker.id, [event1.id, event2.id]) |
31 |
- @checker.reload.memory[:queue].should == ["Something you should know about", "Something else you should know about"] |
|
31 |
+ @checker.reload.memory[:queue].should == [{ 'data' => "Something you should know about" }, { 'data' => "Something else you should know about" }] |
|
32 | 32 |
end |
33 | 33 |
end |
34 | 34 |
|
@@ -37,7 +37,7 @@ describe Agents::DigestEmailAgent do |
||
37 | 37 |
Agents::DigestEmailAgent.async_check(@checker.id) |
38 | 38 |
ActionMailer::Base.deliveries.should == [] |
39 | 39 |
|
40 |
- @checker.memory[:queue] = ["Something you should know about", |
|
40 |
+ @checker.memory[:queue] = [{ :data => "Something you should know about" }, |
|
41 | 41 |
{ :title => "Foo", :url => "http://google.com", :bar => 2 }, |
42 | 42 |
{ "message" => "hi", :woah => "there" }, |
43 | 43 |
{ "test" => 2 }] |
@@ -47,7 +47,7 @@ describe Agents::DigestEmailAgent do |
||
47 | 47 |
Agents::DigestEmailAgent.async_check(@checker.id) |
48 | 48 |
ActionMailer::Base.deliveries.last.to.should == ["bob@example.com"] |
49 | 49 |
ActionMailer::Base.deliveries.last.subject.should == "something interesting" |
50 |
- get_message_part(ActionMailer::Base.deliveries.last, /plain/).strip.should == "Something you should know about\n\nFoo\n bar: 2\n url: http://google.com\n\nhi\n woah: there\n\nEvent\n test: 2" |
|
50 |
+ get_message_part(ActionMailer::Base.deliveries.last, /plain/).strip.should == "Event\n data: Something you should know about\n\nFoo\n bar: 2\n url: http://google.com\n\nhi\n woah: there\n\nEvent\n test: 2" |
|
51 | 51 |
@checker.reload.memory[:queue].should be_empty |
52 | 52 |
end |
53 | 53 |
|
@@ -21,12 +21,12 @@ describe Agents::EmailAgent do |
||
21 | 21 |
|
22 | 22 |
event1 = Event.new |
23 | 23 |
event1.agent = agents(:bob_rain_notifier_agent) |
24 |
- event1.payload = "Something you should know about" |
|
24 |
+ event1.payload = { :data => "Something you should know about" } |
|
25 | 25 |
event1.save! |
26 | 26 |
|
27 | 27 |
event2 = Event.new |
28 | 28 |
event2.agent = agents(:bob_weather_agent) |
29 |
- event2.payload = "Something else you should know about" |
|
29 |
+ event2.payload = { :data => "Something else you should know about" } |
|
30 | 30 |
event2.save! |
31 | 31 |
|
32 | 32 |
Agents::EmailAgent.async_receive(@checker.id, [event1.id]) |
@@ -35,8 +35,8 @@ describe Agents::EmailAgent do |
||
35 | 35 |
ActionMailer::Base.deliveries.count.should == 2 |
36 | 36 |
ActionMailer::Base.deliveries.last.to.should == ["bob@example.com"] |
37 | 37 |
ActionMailer::Base.deliveries.last.subject.should == "something interesting" |
38 |
- get_message_part(ActionMailer::Base.deliveries.last, /plain/).strip.should == "Something else you should know about" |
|
39 |
- get_message_part(ActionMailer::Base.deliveries.first, /plain/).strip.should == "Something you should know about" |
|
38 |
+ get_message_part(ActionMailer::Base.deliveries.last, /plain/).strip.should == "Event\n data: Something else you should know about" |
|
39 |
+ get_message_part(ActionMailer::Base.deliveries.first, /plain/).strip.should == "Event\n data: Something you should know about" |
|
40 | 40 |
end |
41 | 41 |
|
42 | 42 |
it "can receive complex events and send them on" do |
@@ -9,8 +9,8 @@ describe Agents::HumanTaskAgent do |
||
9 | 9 |
|
10 | 10 |
@event = Event.new |
11 | 11 |
@event.agent = agents(:bob_rain_notifier_agent) |
12 |
- @event.payload = { :foo => { "bar" => { :baz => "a2b" } }, |
|
13 |
- :name => "Joe" } |
|
12 |
+ @event.payload = { 'foo' => { "bar" => { 'baz' => "a2b" } }, |
|
13 |
+ 'name' => "Joe" } |
|
14 | 14 |
@event.id = 345 |
15 | 15 |
|
16 | 16 |
@checker.should be_valid |
@@ -18,115 +18,151 @@ describe Agents::HumanTaskAgent do |
||
18 | 18 |
|
19 | 19 |
describe "validations" do |
20 | 20 |
it "validates that trigger_on is 'schedule' or 'event'" do |
21 |
- @checker.options[:trigger_on] = "foo" |
|
21 |
+ @checker.options['trigger_on'] = "foo" |
|
22 | 22 |
@checker.should_not be_valid |
23 | 23 |
end |
24 | 24 |
|
25 | 25 |
it "requires expected_receive_period_in_days when trigger_on is set to 'event'" do |
26 |
- @checker.options[:trigger_on] = "event" |
|
27 |
- @checker.options[:expected_receive_period_in_days] = nil |
|
26 |
+ @checker.options['trigger_on'] = "event" |
|
27 |
+ @checker.options['expected_receive_period_in_days'] = nil |
|
28 | 28 |
@checker.should_not be_valid |
29 |
- @checker.options[:expected_receive_period_in_days] = 2 |
|
29 |
+ @checker.options['expected_receive_period_in_days'] = 2 |
|
30 | 30 |
@checker.should be_valid |
31 | 31 |
end |
32 | 32 |
|
33 | 33 |
it "requires a positive submission_period when trigger_on is set to 'schedule'" do |
34 |
- @checker.options[:trigger_on] = "schedule" |
|
35 |
- @checker.options[:submission_period] = nil |
|
34 |
+ @checker.options['trigger_on'] = "schedule" |
|
35 |
+ @checker.options['submission_period'] = nil |
|
36 | 36 |
@checker.should_not be_valid |
37 |
- @checker.options[:submission_period] = 2 |
|
37 |
+ @checker.options['submission_period'] = 2 |
|
38 | 38 |
@checker.should be_valid |
39 | 39 |
end |
40 | 40 |
|
41 | 41 |
it "requires a hit.title" do |
42 |
- @checker.options[:hit][:title] = "" |
|
42 |
+ @checker.options['hit']['title'] = "" |
|
43 | 43 |
@checker.should_not be_valid |
44 | 44 |
end |
45 | 45 |
|
46 | 46 |
it "requires a hit.description" do |
47 |
- @checker.options[:hit][:description] = "" |
|
47 |
+ @checker.options['hit']['description'] = "" |
|
48 | 48 |
@checker.should_not be_valid |
49 | 49 |
end |
50 | 50 |
|
51 | 51 |
it "requires hit.assignments" do |
52 |
- @checker.options[:hit][:assignments] = "" |
|
52 |
+ @checker.options['hit']['assignments'] = "" |
|
53 | 53 |
@checker.should_not be_valid |
54 |
- @checker.options[:hit][:assignments] = 0 |
|
54 |
+ @checker.options['hit']['assignments'] = 0 |
|
55 | 55 |
@checker.should_not be_valid |
56 |
- @checker.options[:hit][:assignments] = "moose" |
|
56 |
+ @checker.options['hit']['assignments'] = "moose" |
|
57 | 57 |
@checker.should_not be_valid |
58 |
- @checker.options[:hit][:assignments] = "2" |
|
58 |
+ @checker.options['hit']['assignments'] = "2" |
|
59 | 59 |
@checker.should be_valid |
60 | 60 |
end |
61 | 61 |
|
62 | 62 |
it "requires hit.questions" do |
63 |
- old_questions = @checker.options[:hit][:questions] |
|
64 |
- @checker.options[:hit][:questions] = nil |
|
63 |
+ old_questions = @checker.options['hit']['questions'] |
|
64 |
+ @checker.options['hit']['questions'] = nil |
|
65 | 65 |
@checker.should_not be_valid |
66 |
- @checker.options[:hit][:questions] = [] |
|
66 |
+ @checker.options['hit']['questions'] = [] |
|
67 | 67 |
@checker.should_not be_valid |
68 |
- @checker.options[:hit][:questions] = [old_questions[0]] |
|
68 |
+ @checker.options['hit']['questions'] = [old_questions[0]] |
|
69 | 69 |
@checker.should be_valid |
70 | 70 |
end |
71 | 71 |
|
72 | 72 |
it "requires that all questions have key, name, required, type, and question" do |
73 |
- old_questions = @checker.options[:hit][:questions] |
|
74 |
- @checker.options[:hit][:questions].first[:key] = "" |
|
73 |
+ old_questions = @checker.options['hit']['questions'] |
|
74 |
+ @checker.options['hit']['questions'].first['key'] = "" |
|
75 | 75 |
@checker.should_not be_valid |
76 | 76 |
|
77 |
- @checker.options[:hit][:questions] = old_questions |
|
78 |
- @checker.options[:hit][:questions].first[:name] = "" |
|
77 |
+ @checker.options['hit']['questions'] = old_questions |
|
78 |
+ @checker.options['hit']['questions'].first['name'] = "" |
|
79 | 79 |
@checker.should_not be_valid |
80 | 80 |
|
81 |
- @checker.options[:hit][:questions] = old_questions |
|
82 |
- @checker.options[:hit][:questions].first[:required] = nil |
|
81 |
+ @checker.options['hit']['questions'] = old_questions |
|
82 |
+ @checker.options['hit']['questions'].first['required'] = nil |
|
83 | 83 |
@checker.should_not be_valid |
84 | 84 |
|
85 |
- @checker.options[:hit][:questions] = old_questions |
|
86 |
- @checker.options[:hit][:questions].first[:type] = "" |
|
85 |
+ @checker.options['hit']['questions'] = old_questions |
|
86 |
+ @checker.options['hit']['questions'].first['type'] = "" |
|
87 | 87 |
@checker.should_not be_valid |
88 | 88 |
|
89 |
- @checker.options[:hit][:questions] = old_questions |
|
90 |
- @checker.options[:hit][:questions].first[:question] = "" |
|
89 |
+ @checker.options['hit']['questions'] = old_questions |
|
90 |
+ @checker.options['hit']['questions'].first['question'] = "" |
|
91 | 91 |
@checker.should_not be_valid |
92 | 92 |
end |
93 | 93 |
|
94 | 94 |
it "requires that all questions of type 'selection' have a selections array with keys and text" do |
95 |
- @checker.options[:hit][:questions][0][:selections] = [] |
|
95 |
+ @checker.options['hit']['questions'][0]['selections'] = [] |
|
96 | 96 |
@checker.should_not be_valid |
97 |
- @checker.options[:hit][:questions][0][:selections] = [{}] |
|
97 |
+ @checker.options['hit']['questions'][0]['selections'] = [{}] |
|
98 | 98 |
@checker.should_not be_valid |
99 |
- @checker.options[:hit][:questions][0][:selections] = [{ :key => "", :text => "" }] |
|
99 |
+ @checker.options['hit']['questions'][0]['selections'] = [{ 'key' => "", 'text' => "" }] |
|
100 | 100 |
@checker.should_not be_valid |
101 |
- @checker.options[:hit][:questions][0][:selections] = [{ :key => "", :text => "hi" }] |
|
101 |
+ @checker.options['hit']['questions'][0]['selections'] = [{ 'key' => "", 'text' => "hi" }] |
|
102 | 102 |
@checker.should_not be_valid |
103 |
- @checker.options[:hit][:questions][0][:selections] = [{ :key => "hi", :text => "" }] |
|
103 |
+ @checker.options['hit']['questions'][0]['selections'] = [{ 'key' => "hi", 'text' => "" }] |
|
104 | 104 |
@checker.should_not be_valid |
105 |
- @checker.options[:hit][:questions][0][:selections] = [{ :key => "hi", :text => "hi" }] |
|
105 |
+ @checker.options['hit']['questions'][0]['selections'] = [{ 'key' => "hi", 'text' => "hi" }] |
|
106 | 106 |
@checker.should be_valid |
107 |
- @checker.options[:hit][:questions][0][:selections] = [{ :key => "hi", :text => "hi" }, {}] |
|
107 |
+ @checker.options['hit']['questions'][0]['selections'] = [{ 'key' => "hi", 'text' => "hi" }, {}] |
|
108 | 108 |
@checker.should_not be_valid |
109 | 109 |
end |
110 | 110 |
|
111 |
- it "requires that all questions be of type 'selection' when `take_majority` is `true`" do |
|
112 |
- @checker.options[:take_majority] = "true" |
|
111 |
+ it "requires that 'poll_options' be present and populated when 'combination_mode' is set to 'poll'" do |
|
112 |
+ @checker.options['combination_mode'] = "poll" |
|
113 | 113 |
@checker.should_not be_valid |
114 |
- @checker.options[:hit][:questions][1][:type] = "selection" |
|
115 |
- @checker.options[:hit][:questions][1][:selections] = @checker.options[:hit][:questions][0][:selections] |
|
114 |
+ @checker.options['poll_options'] = {} |
|
115 |
+ @checker.should_not be_valid |
|
116 |
+ @checker.options['poll_options'] = { 'title' => "Take a poll about jokes", |
|
117 |
+ 'instructions' => "Rank these by how funny they are", |
|
118 |
+ 'assignments' => 3, |
|
119 |
+ 'row_template' => "<$.joke>" } |
|
120 |
+ @checker.should be_valid |
|
121 |
+ @checker.options['poll_options'] = { 'instructions' => "Rank these by how funny they are", |
|
122 |
+ 'assignments' => 3, |
|
123 |
+ 'row_template' => "<$.joke>" } |
|
124 |
+ @checker.should_not be_valid |
|
125 |
+ @checker.options['poll_options'] = { 'title' => "Take a poll about jokes", |
|
126 |
+ 'assignments' => 3, |
|
127 |
+ 'row_template' => "<$.joke>" } |
|
128 |
+ @checker.should_not be_valid |
|
129 |
+ @checker.options['poll_options'] = { 'title' => "Take a poll about jokes", |
|
130 |
+ 'instructions' => "Rank these by how funny they are", |
|
131 |
+ 'row_template' => "<$.joke>" } |
|
132 |
+ @checker.should_not be_valid |
|
133 |
+ @checker.options['poll_options'] = { 'title' => "Take a poll about jokes", |
|
134 |
+ 'instructions' => "Rank these by how funny they are", |
|
135 |
+ 'assignments' => 3} |
|
136 |
+ @checker.should_not be_valid |
|
137 |
+ end |
|
138 |
+ |
|
139 |
+ it "requires that all questions be of type 'selection' when 'combination_mode' is 'take_majority'" do |
|
140 |
+ @checker.options['combination_mode'] = "take_majority" |
|
141 |
+ @checker.should_not be_valid |
|
142 |
+ @checker.options['hit']['questions'][1]['type'] = "selection" |
|
143 |
+ @checker.options['hit']['questions'][1]['selections'] = @checker.options['hit']['questions'][0]['selections'] |
|
144 |
+ @checker.should be_valid |
|
145 |
+ end |
|
146 |
+ |
|
147 |
+ it "accepts 'take_majority': 'true' for legacy support" do |
|
148 |
+ @checker.options['take_majority'] = "true" |
|
149 |
+ @checker.should_not be_valid |
|
150 |
+ @checker.options['hit']['questions'][1]['type'] = "selection" |
|
151 |
+ @checker.options['hit']['questions'][1]['selections'] = @checker.options['hit']['questions'][0]['selections'] |
|
116 | 152 |
@checker.should be_valid |
117 | 153 |
end |
118 | 154 |
end |
119 | 155 |
|
120 | 156 |
describe "when 'trigger_on' is set to 'schedule'" do |
121 | 157 |
before do |
122 |
- @checker.options[:trigger_on] = "schedule" |
|
123 |
- @checker.options[:submission_period] = "2" |
|
124 |
- @checker.options.delete(:expected_receive_period_in_days) |
|
158 |
+ @checker.options['trigger_on'] = "schedule" |
|
159 |
+ @checker.options['submission_period'] = "2" |
|
160 |
+ @checker.options.delete('expected_receive_period_in_days') |
|
125 | 161 |
end |
126 | 162 |
|
127 | 163 |
it "should check for reviewable HITs frequently" do |
128 | 164 |
mock(@checker).review_hits.twice |
129 |
- mock(@checker).create_hit.once |
|
165 |
+ mock(@checker).create_basic_hit.once |
|
130 | 166 |
@checker.check |
131 | 167 |
@checker.check |
132 | 168 |
end |
@@ -135,7 +171,7 @@ describe Agents::HumanTaskAgent do |
||
135 | 171 |
now = Time.now |
136 | 172 |
stub(Time).now { now } |
137 | 173 |
mock(@checker).review_hits.times(3) |
138 |
- mock(@checker).create_hit.twice |
|
174 |
+ mock(@checker).create_basic_hit.twice |
|
139 | 175 |
@checker.check |
140 | 176 |
now += 1 * 60 * 60 |
141 | 177 |
@checker.check |
@@ -144,18 +180,18 @@ describe Agents::HumanTaskAgent do |
||
144 | 180 |
end |
145 | 181 |
|
146 | 182 |
it "should ignore events" do |
147 |
- mock(@checker).create_hit(anything).times(0) |
|
183 |
+ mock(@checker).create_basic_hit(anything).times(0) |
|
148 | 184 |
@checker.receive([events(:bob_website_agent_event)]) |
149 | 185 |
end |
150 | 186 |
end |
151 | 187 |
|
152 | 188 |
describe "when 'trigger_on' is set to 'event'" do |
153 | 189 |
it "should not create HITs during check but should check for reviewable HITs" do |
154 |
- @checker.options[:submission_period] = "2" |
|
190 |
+ @checker.options['submission_period'] = "2" |
|
155 | 191 |
now = Time.now |
156 | 192 |
stub(Time).now { now } |
157 | 193 |
mock(@checker).review_hits.times(3) |
158 |
- mock(@checker).create_hit.times(0) |
|
194 |
+ mock(@checker).create_basic_hit.times(0) |
|
159 | 195 |
@checker.check |
160 | 196 |
now += 1 * 60 * 60 |
161 | 197 |
@checker.check |
@@ -164,16 +200,16 @@ describe Agents::HumanTaskAgent do |
||
164 | 200 |
end |
165 | 201 |
|
166 | 202 |
it "should create HITs based on events" do |
167 |
- mock(@checker).create_hit(events(:bob_website_agent_event)).times(1) |
|
203 |
+ mock(@checker).create_basic_hit(events(:bob_website_agent_event)).times(1) |
|
168 | 204 |
@checker.receive([events(:bob_website_agent_event)]) |
169 | 205 |
end |
170 | 206 |
end |
171 | 207 |
|
172 | 208 |
describe "creating hits" do |
173 | 209 |
it "can create HITs based on events, interpolating their values" do |
174 |
- @checker.options[:hit][:title] = "Hi <.name>" |
|
175 |
- @checker.options[:hit][:description] = "Make something for <.name>" |
|
176 |
- @checker.options[:hit][:questions][0][:name] = "<.name> Question 1" |
|
210 |
+ @checker.options['hit']['title'] = "Hi <.name>" |
|
211 |
+ @checker.options['hit']['description'] = "Make something for <.name>" |
|
212 |
+ @checker.options['hit']['questions'][0]['name'] = "<.name> Question 1" |
|
177 | 213 |
|
178 | 214 |
question_form = nil |
179 | 215 |
hitInterface = OpenStruct.new |
@@ -181,10 +217,10 @@ describe Agents::HumanTaskAgent do |
||
181 | 217 |
mock(hitInterface).question_form(instance_of Agents::HumanTaskAgent::AgentQuestionForm) { |agent_question_form_instance| question_form = agent_question_form_instance } |
182 | 218 |
mock(RTurk::Hit).create(:title => "Hi Joe").yields(hitInterface) { hitInterface } |
183 | 219 |
|
184 |
- @checker.send :create_hit, @event |
|
220 |
+ @checker.send :create_basic_hit, @event |
|
185 | 221 |
|
186 |
- hitInterface.max_assignments.should == @checker.options[:hit][:assignments] |
|
187 |
- hitInterface.reward.should == @checker.options[:hit][:reward] |
|
222 |
+ hitInterface.max_assignments.should == @checker.options['hit']['assignments'] |
|
223 |
+ hitInterface.reward.should == @checker.options['hit']['reward'] |
|
188 | 224 |
hitInterface.description.should == "Make something for Joe" |
189 | 225 |
|
190 | 226 |
xml = question_form.to_xml |
@@ -192,18 +228,18 @@ describe Agents::HumanTaskAgent do |
||
192 | 228 |
xml.should include("<Text>Make something for Joe</Text>") |
193 | 229 |
xml.should include("<DisplayName>Joe Question 1</DisplayName>") |
194 | 230 |
|
195 |
- @checker.memory[:hits][123].should == @event.id |
|
231 |
+ @checker.memory['hits'][123]['event_id'].should == @event.id |
|
196 | 232 |
end |
197 | 233 |
|
198 | 234 |
it "works without an event too" do |
199 |
- @checker.options[:hit][:title] = "Hi <.name>" |
|
235 |
+ @checker.options['hit']['title'] = "Hi <.name>" |
|
200 | 236 |
hitInterface = OpenStruct.new |
201 | 237 |
hitInterface.id = 123 |
202 | 238 |
mock(hitInterface).question_form(instance_of Agents::HumanTaskAgent::AgentQuestionForm) |
203 | 239 |
mock(RTurk::Hit).create(:title => "Hi").yields(hitInterface) { hitInterface } |
204 |
- @checker.send :create_hit |
|
205 |
- hitInterface.max_assignments.should == @checker.options[:hit][:assignments] |
|
206 |
- hitInterface.reward.should == @checker.options[:hit][:reward] |
|
240 |
+ @checker.send :create_basic_hit |
|
241 |
+ hitInterface.max_assignments.should == @checker.options['hit']['assignments'] |
|
242 |
+ hitInterface.reward.should == @checker.options['hit']['reward'] |
|
207 | 243 |
end |
208 | 244 |
end |
209 | 245 |
|
@@ -253,14 +289,14 @@ describe Agents::HumanTaskAgent do |
||
253 | 289 |
it "should work on multiple HITs" do |
254 | 290 |
event2 = Event.new |
255 | 291 |
event2.agent = agents(:bob_rain_notifier_agent) |
256 |
- event2.payload = { :foo2 => { "bar2" => { :baz2 => "a2b2" } }, |
|
257 |
- :name2 => "Joe2" } |
|
292 |
+ event2.payload = { 'foo2' => { "bar2" => { 'baz2' => "a2b2" } }, |
|
293 |
+ 'name2' => "Joe2" } |
|
258 | 294 |
event2.id = 3452 |
259 | 295 |
|
260 | 296 |
# It knows about two HITs from two different events. |
261 |
- @checker.memory[:hits] = {} |
|
262 |
- @checker.memory[:hits][:"JH3132836336DHG"] = @event.id |
|
263 |
- @checker.memory[:hits][:"JH39AA63836DHG"] = event2.id |
|
297 |
+ @checker.memory['hits'] = {} |
|
298 |
+ @checker.memory['hits']["JH3132836336DHG"] = { 'event_id' => @event.id } |
|
299 |
+ @checker.memory['hits']["JH39AA63836DHG"] = { 'event_id' => event2.id } |
|
264 | 300 |
|
265 | 301 |
hit_ids = %w[JH3132836336DHG JH39AA63836DHG JH39AA63836DH12345] |
266 | 302 |
mock(RTurk::GetReviewableHITs).create { mock!.hit_ids { hit_ids } } # It sees 3 HITs. |
@@ -273,7 +309,7 @@ describe Agents::HumanTaskAgent do |
||
273 | 309 |
end |
274 | 310 |
|
275 | 311 |
it "shouldn't do anything if an assignment isn't ready" do |
276 |
- @checker.memory[:hits] = { :"JH3132836336DHG" => @event.id } |
|
312 |
+ @checker.memory['hits'] = { "JH3132836336DHG" => { 'event_id' => @event.id } } |
|
277 | 313 |
mock(RTurk::GetReviewableHITs).create { mock!.hit_ids { %w[JH3132836336DHG JH39AA63836DHG JH39AA63836DH12345] } } |
278 | 314 |
assignments = [ |
279 | 315 |
FakeAssignment.new(:status => "Accepted", :answers => {}), |
@@ -288,11 +324,11 @@ describe Agents::HumanTaskAgent do |
||
288 | 324 |
@checker.send :review_hits |
289 | 325 |
|
290 | 326 |
assignments.all? {|a| a.approved == true }.should be_false |
291 |
- @checker.memory[:hits].should == { :"JH3132836336DHG" => @event.id } |
|
327 |
+ @checker.memory['hits'].should == { "JH3132836336DHG" => { 'event_id' => @event.id } } |
|
292 | 328 |
end |
293 | 329 |
|
294 | 330 |
it "shouldn't do anything if an assignment is missing" do |
295 |
- @checker.memory[:hits] = { :"JH3132836336DHG" => @event.id } |
|
331 |
+ @checker.memory['hits'] = { "JH3132836336DHG" => { 'event_id' => @event.id } } |
|
296 | 332 |
mock(RTurk::GetReviewableHITs).create { mock!.hit_ids { %w[JH3132836336DHG JH39AA63836DHG JH39AA63836DH12345] } } |
297 | 333 |
assignments = [ |
298 | 334 |
FakeAssignment.new(:status => "Submitted", :answers => {"sentiment"=>"happy", "feedback"=>"Take 2"}) |
@@ -306,11 +342,11 @@ describe Agents::HumanTaskAgent do |
||
306 | 342 |
@checker.send :review_hits |
307 | 343 |
|
308 | 344 |
assignments.all? {|a| a.approved == true }.should be_false |
309 |
- @checker.memory[:hits].should == { :"JH3132836336DHG" => @event.id } |
|
345 |
+ @checker.memory['hits'].should == { "JH3132836336DHG" => { 'event_id' => @event.id } } |
|
310 | 346 |
end |
311 | 347 |
|
312 | 348 |
it "should create events when all assignments are ready" do |
313 |
- @checker.memory[:hits] = { :"JH3132836336DHG" => @event.id } |
|
349 |
+ @checker.memory['hits'] = { "JH3132836336DHG" => { 'event_id' => @event.id } } |
|
314 | 350 |
mock(RTurk::GetReviewableHITs).create { mock!.hit_ids { %w[JH3132836336DHG JH39AA63836DHG JH39AA63836DH12345] } } |
315 | 351 |
assignments = [ |
316 | 352 |
FakeAssignment.new(:status => "Submitted", :answers => {"sentiment"=>"neutral", "feedback"=>""}), |
@@ -327,32 +363,32 @@ describe Agents::HumanTaskAgent do |
||
327 | 363 |
assignments.all? {|a| a.approved == true }.should be_true |
328 | 364 |
hit.should be_disposed |
329 | 365 |
|
330 |
- @checker.events.last.payload[:answers].should == [ |
|
331 |
- {:sentiment => "neutral", :feedback => ""}, |
|
332 |
- {:sentiment => "happy", :feedback => "Take 2"} |
|
366 |
+ @checker.events.last.payload['answers'].should == [ |
|
367 |
+ {'sentiment' => "neutral", 'feedback' => ""}, |
|
368 |
+ {'sentiment' => "happy", 'feedback' => "Take 2"} |
|
333 | 369 |
] |
334 | 370 |
|
335 |
- @checker.memory[:hits].should == {} |
|
371 |
+ @checker.memory['hits'].should == {} |
|
336 | 372 |
end |
337 | 373 |
|
338 | 374 |
describe "taking majority votes" do |
339 | 375 |
before do |
340 |
- @checker.options[:take_majority] = "true" |
|
341 |
- @checker.memory[:hits] = { :"JH3132836336DHG" => @event.id } |
|
376 |
+ @checker.options['combination_mode'] = "take_majority" |
|
377 |
+ @checker.memory['hits'] = { "JH3132836336DHG" => { 'event_id' => @event.id } } |
|
342 | 378 |
mock(RTurk::GetReviewableHITs).create { mock!.hit_ids { %w[JH3132836336DHG JH39AA63836DHG JH39AA63836DH12345] } } |
343 | 379 |
end |
344 | 380 |
|
345 | 381 |
it "should take the majority votes of all questions" do |
346 |
- @checker.options[:hit][:questions][1] = { |
|
347 |
- :type => "selection", |
|
348 |
- :key => "age_range", |
|
349 |
- :name => "Age Range", |
|
350 |
- :required => "true", |
|
351 |
- :question => "Please select your age range:", |
|
352 |
- :selections => |
|
382 |
+ @checker.options['hit']['questions'][1] = { |
|
383 |
+ 'type' => "selection", |
|
384 |
+ 'key' => "age_range", |
|
385 |
+ 'name' => "Age Range", |
|
386 |
+ 'required' => "true", |
|
387 |
+ 'question' => "Please select your age range:", |
|
388 |
+ 'selections' => |
|
353 | 389 |
[ |
354 |
- { :key => "<50", :text => "50 years old or younger" }, |
|
355 |
- { :key => ">50", :text => "Over 50 years old" } |
|
390 |
+ { 'key' => "<50", 'text' => "50 years old or younger" }, |
|
391 |
+ { 'key' => ">50", 'text' => "Over 50 years old" } |
|
356 | 392 |
] |
357 | 393 |
} |
358 | 394 |
|
@@ -371,35 +407,39 @@ describe Agents::HumanTaskAgent do |
||
371 | 407 |
|
372 | 408 |
assignments.all? {|a| a.approved == true }.should be_true |
373 | 409 |
|
374 |
- @checker.events.last.payload[:answers].should == [ |
|
375 |
- { :sentiment => "sad", :age_range => "<50" }, |
|
376 |
- { :sentiment => "neutral", :age_range => ">50" }, |
|
377 |
- { :sentiment => "happy", :age_range => ">50" }, |
|
378 |
- { :sentiment => "happy", :age_range => ">50" } |
|
410 |
+ @checker.events.last.payload['answers'].should == [ |
|
411 |
+ { 'sentiment' => "sad", 'age_range' => "<50" }, |
|
412 |
+ { 'sentiment' => "neutral", 'age_range' => ">50" }, |
|
413 |
+ { 'sentiment' => "happy", 'age_range' => ">50" }, |
|
414 |
+ { 'sentiment' => "happy", 'age_range' => ">50" } |
|
379 | 415 |
] |
380 | 416 |
|
381 |
- @checker.events.last.payload[:counts].should == { :sentiment => { :happy => 2, :sad => 1, :neutral => 1 }, :age_range => { :">50" => 3, :"<50" => 1 } } |
|
382 |
- @checker.events.last.payload[:majority_answer].should == { :sentiment => "happy", :age_range => ">50" } |
|
383 |
- @checker.events.last.payload.should_not have_key(:average_answer) |
|
417 |
+ @checker.events.last.payload['counts'].should == { 'sentiment' => { 'happy' => 2, 'sad' => 1, 'neutral' => 1 }, 'age_range' => { ">50" => 3, "<50" => 1 } } |
|
418 |
+ @checker.events.last.payload['majority_answer'].should == { 'sentiment' => "happy", 'age_range' => ">50" } |
|
419 |
+ @checker.events.last.payload.should_not have_key('average_answer') |
|
384 | 420 |
|
385 |
- @checker.memory[:hits].should == {} |
|
421 |
+ @checker.memory['hits'].should == {} |
|
386 | 422 |
end |
387 | 423 |
|
388 | 424 |
it "should also provide an average answer when all questions are numeric" do |
389 |
- @checker.options[:hit][:questions] = [ |
|
425 |
+ # it should accept 'take_majority': 'true' as well for legacy support. Demonstrating that here. |
|
426 |
+ @checker.options.delete :combination_mode |
|
427 |
+ @checker.options['take_majority'] = "true" |
|
428 |
+ |
|
429 |
+ @checker.options['hit']['questions'] = [ |
|
390 | 430 |
{ |
391 |
- :type => "selection", |
|
392 |
- :key => "rating", |
|
393 |
- :name => "Rating", |
|
394 |
- :required => "true", |
|
395 |
- :question => "Please select a rating:", |
|
396 |
- :selections => |
|
431 |
+ 'type' => "selection", |
|
432 |
+ 'key' => "rating", |
|
433 |
+ 'name' => "Rating", |
|
434 |
+ 'required' => "true", |
|
435 |
+ 'question' => "Please select a rating:", |
|
436 |
+ 'selections' => |
|
397 | 437 |
[ |
398 |
- { :key => "1", :text => "One" }, |
|
399 |
- { :key => "2", :text => "Two" }, |
|
400 |
- { :key => "3", :text => "Three" }, |
|
401 |
- { :key => "4", :text => "Four" }, |
|
402 |
- { :key => "5.1", :text => "Five Point One" } |
|
438 |
+ { 'key' => "1", 'text' => "One" }, |
|
439 |
+ { 'key' => "2", 'text' => "Two" }, |
|
440 |
+ { 'key' => "3", 'text' => "Three" }, |
|
441 |
+ { 'key' => "4", 'text' => "Four" }, |
|
442 |
+ { 'key' => "5.1", 'text' => "Five Point One" } |
|
403 | 443 |
] |
404 | 444 |
} |
405 | 445 |
] |
@@ -420,20 +460,133 @@ describe Agents::HumanTaskAgent do |
||
420 | 460 |
|
421 | 461 |
assignments.all? {|a| a.approved == true }.should be_true |
422 | 462 |
|
423 |
- @checker.events.last.payload[:answers].should == [ |
|
424 |
- { :rating => "1" }, |
|
425 |
- { :rating => "3" }, |
|
426 |
- { :rating => "5.1" }, |
|
427 |
- { :rating => "2" }, |
|
428 |
- { :rating => "2" } |
|
463 |
+ @checker.events.last.payload['answers'].should == [ |
|
464 |
+ { 'rating' => "1" }, |
|
465 |
+ { 'rating' => "3" }, |
|
466 |
+ { 'rating' => "5.1" }, |
|
467 |
+ { 'rating' => "2" }, |
|
468 |
+ { 'rating' => "2" } |
|
429 | 469 |
] |
430 | 470 |
|
431 |
- @checker.events.last.payload[:counts].should == { :rating => { :"1" => 1, :"2" => 2, :"3" => 1, :"4" => 0, :"5.1" => 1 } } |
|
432 |
- @checker.events.last.payload[:majority_answer].should == { :rating => "2" } |
|
433 |
- @checker.events.last.payload[:average_answer].should == { :rating => (1 + 2 + 2 + 3 + 5.1) / 5.0 } |
|
471 |
+ @checker.events.last.payload['counts'].should == { 'rating' => { "1" => 1, "2" => 2, "3" => 1, "4" => 0, "5.1" => 1 } } |
|
472 |
+ @checker.events.last.payload['majority_answer'].should == { 'rating' => "2" } |
|
473 |
+ @checker.events.last.payload['average_answer'].should == { 'rating' => (1 + 2 + 2 + 3 + 5.1) / 5.0 } |
|
474 |
+ |
|
475 |
+ @checker.memory['hits'].should == {} |
|
476 |
+ end |
|
477 |
+ end |
|
478 |
+ |
|
479 |
+ describe "creating and reviewing polls" do |
|
480 |
+ before do |
|
481 |
+ @checker.options['combination_mode'] = "poll" |
|
482 |
+ @checker.options['poll_options'] = { |
|
483 |
+ 'title' => "Hi!", |
|
484 |
+ 'instructions' => "hello!", |
|
485 |
+ 'assignments' => 2, |
|
486 |
+ 'row_template' => "This is <.sentiment>" |
|
487 |
+ } |
|
488 |
+ @event.save! |
|
489 |
+ mock(RTurk::GetReviewableHITs).create { mock!.hit_ids { %w[JH3132836336DHG JH39AA63836DHG JH39AA63836DH12345] } } |
|
490 |
+ end |
|
491 |
+ |
|
492 |
+ it "creates a poll using the row_template, message, and correct number of assignments" do |
|
493 |
+ @checker.memory['hits'] = { "JH3132836336DHG" => { 'event_id' => @event.id } } |
|
494 |
+ |
|
495 |
+ # Mock out the HIT's submitted assignments. |
|
496 |
+ assignments = [ |
|
497 |
+ FakeAssignment.new(:status => "Submitted", :answers => {"sentiment"=>"sad", "feedback"=>"This is my feedback 1"}), |
|
498 |
+ FakeAssignment.new(:status => "Submitted", :answers => {"sentiment"=>"neutral", "feedback"=>"This is my feedback 2"}), |
|
499 |
+ FakeAssignment.new(:status => "Submitted", :answers => {"sentiment"=>"happy", "feedback"=>"This is my feedback 3"}), |
|
500 |
+ FakeAssignment.new(:status => "Submitted", :answers => {"sentiment"=>"happy", "feedback"=>"This is my feedback 4"}) |
|
501 |
+ ] |
|
502 |
+ hit = FakeHit.new(:max_assignments => 4, :assignments => assignments) |
|
503 |
+ mock(RTurk::Hit).new("JH3132836336DHG") { hit } |
|
504 |
+ |
|
505 |
+ @checker.memory['hits']["JH3132836336DHG"].should be_present |
|
506 |
+ |
|
507 |
+ # Setup mocks for HIT creation |
|
508 |
+ |
|
509 |
+ question_form = nil |
|
510 |
+ hitInterface = OpenStruct.new |
|
511 |
+ hitInterface.id = "JH39AA63836DH12345" |
|
512 |
+ mock(hitInterface).question_form(instance_of Agents::HumanTaskAgent::AgentQuestionForm) { |agent_question_form_instance| question_form = agent_question_form_instance } |
|
513 |
+ mock(RTurk::Hit).create(:title => "Hi!").yields(hitInterface) { hitInterface } |
|
514 |
+ |
|
515 |
+ # And finally, the test. |
|
516 |
+ |
|
517 |
+ lambda { |
|
518 |
+ @checker.send :review_hits |
|
519 |
+ }.should change { Event.count }.by(0) # it does not emit an event until all poll results are in |
|
520 |
+ |
|
521 |
+ # it approves the existing assignments |
|
522 |
+ |
|
523 |
+ assignments.all? {|a| a.approved == true }.should be_true |
|
524 |
+ hit.should be_disposed |
|
525 |
+ |
|
526 |
+ # it creates a new HIT for the poll |
|
527 |
+ |
|
528 |
+ hitInterface.max_assignments.should == @checker.options['poll_options']['assignments'] |
|
529 |
+ hitInterface.description.should == @checker.options['poll_options']['instructions'] |
|
530 |
+ |
|
531 |
+ xml = question_form.to_xml |
|
532 |
+ xml.should include("<Text>This is happy</Text>") |
|
533 |
+ xml.should include("<Text>This is neutral</Text>") |
|
534 |
+ xml.should include("<Text>This is sad</Text>") |
|
535 |
+ |
|
536 |
+ @checker.save |
|
537 |
+ @checker.reload |
|
538 |
+ @checker.memory['hits']["JH3132836336DHG"].should_not be_present |
|
539 |
+ @checker.memory['hits']["JH39AA63836DH12345"].should be_present |
|
540 |
+ @checker.memory['hits']["JH39AA63836DH12345"]['event_id'].should == @event.id |
|
541 |
+ @checker.memory['hits']["JH39AA63836DH12345"]['type'].should == "poll" |
|
542 |
+ @checker.memory['hits']["JH39AA63836DH12345"]['original_hit'].should == "JH3132836336DHG" |
|
543 |
+ @checker.memory['hits']["JH39AA63836DH12345"]['answers'].length.should == 4 |
|
544 |
+ end |
|
545 |
+ |
|
546 |
+ it "emits an event when all poll results are in, containing the data from the best answer, plus all others" do |
|
547 |
+ original_answers = [ |
|
548 |
+ { 'sentiment' => "sad", 'feedback' => "This is my feedback 1"}, |
|
549 |
+ { 'sentiment' => "neutral", 'feedback' => "This is my feedback 2"}, |
|
550 |
+ { 'sentiment' => "happy", 'feedback' => "This is my feedback 3"}, |
|
551 |
+ { 'sentiment' => "happy", 'feedback' => "This is my feedback 4"} |
|
552 |
+ ] |
|
553 |
+ |
|
554 |
+ @checker.memory['hits'] = { |
|
555 |
+ 'JH39AA63836DH12345' => { |
|
556 |
+ 'type' => 'poll', |
|
557 |
+ 'original_hit' => "JH3132836336DHG", |
|
558 |
+ 'answers' => original_answers, |
|
559 |
+ 'event_id' => 345 |
|
560 |
+ } |
|
561 |
+ } |
|
562 |
+ |
|
563 |
+ # Mock out the HIT's submitted assignments. |
|
564 |
+ assignments = [ |
|
565 |
+ FakeAssignment.new(:status => "Submitted", :answers => {"1" => "2", "2" => "5", "3" => "3", "4" => "2"}), |
|
566 |
+ FakeAssignment.new(:status => "Submitted", :answers => {"1" => "3", "2" => "4", "3" => "1", "4" => "4"}) |
|
567 |
+ ] |
|
568 |
+ hit = FakeHit.new(:max_assignments => 2, :assignments => assignments) |
|
569 |
+ mock(RTurk::Hit).new("JH39AA63836DH12345") { hit } |
|
570 |
+ |
|
571 |
+ @checker.memory['hits']["JH39AA63836DH12345"].should be_present |
|
572 |
+ |
|
573 |
+ lambda { |
|
574 |
+ @checker.send :review_hits |
|
575 |
+ }.should change { Event.count }.by(1) |
|
576 |
+ |
|
577 |
+ # It emits an event |
|
578 |
+ |
|
579 |
+ @checker.events.last.payload['answers'].should == original_answers |
|
580 |
+ @checker.events.last.payload['poll'].should == [{"1" => "2", "2" => "5", "3" => "3", "4" => "2"}, {"1" => "3", "2" => "4", "3" => "1", "4" => "4"}] |
|
581 |
+ @checker.events.last.payload['best_answer'].should == {'sentiment' => "neutral", 'feedback' => "This is my feedback 2"} |
|
582 |
+ |
|
583 |
+ # it approves the existing assignments |
|
584 |
+ |
|
585 |
+ assignments.all? {|a| a.approved == true }.should be_true |
|
586 |
+ hit.should be_disposed |
|
434 | 587 |
|
435 |
- @checker.memory[:hits].should == {} |
|
588 |
+ @checker.memory['hits'].should be_empty |
|
436 | 589 |
end |
437 | 590 |
end |
438 | 591 |
end |
439 |
-end |
|
592 |
+end |
@@ -3,12 +3,12 @@ require 'spec_helper' |
||
3 | 3 |
describe Agents::PeakDetectorAgent do |
4 | 4 |
before do |
5 | 5 |
@valid_params = { |
6 |
- :name => "my peak detector agent", |
|
7 |
- :options => { |
|
8 |
- :expected_receive_period_in_days => "2", |
|
9 |
- :group_by_path => "filter", |
|
10 |
- :value_path => "count", |
|
11 |
- :message => "A peak was found" |
|
6 |
+ 'name' => "my peak detector agent", |
|
7 |
+ 'options' => { |
|
8 |
+ 'expected_receive_period_in_days' => "2", |
|
9 |
+ 'group_by_path' => "filter", |
|
10 |
+ 'value_path' => "count", |
|
11 |
+ 'message' => "A peak was found" |
|
12 | 12 |
} |
13 | 13 |
} |
14 | 14 |
|
@@ -19,54 +19,54 @@ describe Agents::PeakDetectorAgent do |
||
19 | 19 |
|
20 | 20 |
describe "#receive" do |
21 | 21 |
it "tracks and groups by the group_by_path" do |
22 |
- events = build_events(:keys => [:count, :filter], |
|
22 |
+ events = build_events(:keys => ['count', 'filter'], |
|
23 | 23 |
:values => [[1, "something"], [2, "something"], [3, "else"]]) |
24 | 24 |
@agent.receive events |
25 |
- @agent.memory[:data][:something].map(&:first).should == [1, 2] |
|
26 |
- @agent.memory[:data][:something].last.last.should be_within(10).of((100 - 1).hours.ago.to_i) |
|
27 |
- @agent.memory[:data][:else].first.first.should == 3 |
|
28 |
- @agent.memory[:data][:else].first.last.should be_within(10).of((100 - 2).hours.ago.to_i) |
|
25 |
+ @agent.memory['data']['something'].map(&:first).should == [1, 2] |
|
26 |
+ @agent.memory['data']['something'].last.last.should be_within(10).of((100 - 1).hours.ago.to_i) |
|
27 |
+ @agent.memory['data']['else'].first.first.should == 3 |
|
28 |
+ @agent.memory['data']['else'].first.last.should be_within(10).of((100 - 2).hours.ago.to_i) |
|
29 | 29 |
end |
30 | 30 |
|
31 | 31 |
it "works without a group_by_path as well" do |
32 |
- @agent.options[:group_by_path] = "" |
|
33 |
- events = build_events(:keys => [:count], :values => [[1], [2]]) |
|
32 |
+ @agent.options['group_by_path'] = "" |
|
33 |
+ events = build_events(:keys => ['count'], :values => [[1], [2]]) |
|
34 | 34 |
@agent.receive events |
35 |
- @agent.memory[:data][:no_group].map(&:first).should == [1, 2] |
|
35 |
+ @agent.memory['data']['no_group'].map(&:first).should == [1, 2] |
|
36 | 36 |
end |
37 | 37 |
|
38 | 38 |
it "keeps a rolling window of data" do |
39 |
- @agent.options[:window_duration_in_days] = 5/24.0 |
|
40 |
- @agent.receive build_events(:keys => [:count], |
|
39 |
+ @agent.options['window_duration_in_days'] = 5/24.0 |
|
40 |
+ @agent.receive build_events(:keys => ['count'], |
|
41 | 41 |
:values => [1, 2, 3, 4, 5, 6, 7, 8].map {|i| [i]}, |
42 |
- :pattern => { :filter => "something" }) |
|
43 |
- @agent.memory[:data][:something].map(&:first).should == [4, 5, 6, 7, 8] |
|
42 |
+ :pattern => { 'filter' => "something" }) |
|
43 |
+ @agent.memory['data']['something'].map(&:first).should == [4, 5, 6, 7, 8] |
|
44 | 44 |
end |
45 | 45 |
|
46 | 46 |
it "finds peaks" do |
47 |
- build_events(:keys => [:count], |
|
47 |
+ build_events(:keys => ['count'], |
|
48 | 48 |
:values => [5, 6, |
49 | 49 |
4, 5, |
50 | 50 |
4, 5, |
51 | 51 |
15, 11, # peak |
52 | 52 |
8, 50, # ignored because it's too close to the first peak |
53 | 53 |
4, 5].map {|i| [i]}, |
54 |
- :pattern => { :filter => "something" }).each.with_index do |event, index| |
|
54 |
+ :pattern => { 'filter' => "something" }).each.with_index do |event, index| |
|
55 | 55 |
lambda { |
56 | 56 |
@agent.receive([event]) |
57 | 57 |
}.should change { @agent.events.count }.by( index == 6 ? 1 : 0 ) |
58 | 58 |
end |
59 | 59 |
|
60 |
- @agent.events.last.payload[:peak].should == 15.0 |
|
61 |
- @agent.memory[:peaks][:something].length.should == 1 |
|
60 |
+ @agent.events.last.payload['peak'].should == 15.0 |
|
61 |
+ @agent.memory['peaks']['something'].length.should == 1 |
|
62 | 62 |
end |
63 | 63 |
|
64 | 64 |
it "keeps a rolling window of peaks" do |
65 |
- @agent.options[:min_peak_spacing_in_days] = 1/24.0 |
|
66 |
- @agent.receive build_events(:keys => [:count], |
|
65 |
+ @agent.options['min_peak_spacing_in_days'] = 1/24.0 |
|
66 |
+ @agent.receive build_events(:keys => ['count'], |
|
67 | 67 |
:values => [1, 1, 1, 1, 1, 1, 10, 1, 1, 1, 1, 1, 1, 1, 10, 1].map {|i| [i]}, |
68 |
- :pattern => { :filter => "something" }) |
|
69 |
- @agent.memory[:peaks][:something].length.should == 2 |
|
68 |
+ :pattern => { 'filter' => "something" }) |
|
69 |
+ @agent.memory['peaks']['something'].length.should == 2 |
|
70 | 70 |
end |
71 | 71 |
end |
72 | 72 |
|
@@ -76,17 +76,17 @@ describe Agents::PeakDetectorAgent do |
||
76 | 76 |
end |
77 | 77 |
|
78 | 78 |
it "should validate presence of message" do |
79 |
- @agent.options[:message] = nil |
|
79 |
+ @agent.options['message'] = nil |
|
80 | 80 |
@agent.should_not be_valid |
81 | 81 |
end |
82 | 82 |
|
83 | 83 |
it "should validate presence of expected_receive_period_in_days" do |
84 |
- @agent.options[:expected_receive_period_in_days] = "" |
|
84 |
+ @agent.options['expected_receive_period_in_days'] = "" |
|
85 | 85 |
@agent.should_not be_valid |
86 | 86 |
end |
87 | 87 |
|
88 | 88 |
it "should validate presence of value_path" do |
89 |
- @agent.options[:value_path] = "" |
|
89 |
+ @agent.options['value_path'] = "" |
|
90 | 90 |
@agent.should_not be_valid |
91 | 91 |
end |
92 | 92 |
end |
@@ -53,7 +53,7 @@ describe Agents::SentimentAgent do |
||
53 | 53 |
it "checks if content key is working fine" do |
54 | 54 |
@checker.receive([@event]) |
55 | 55 |
Event.last.payload[:content].should == "value1" |
56 |
- Event.last.payload[:original_event].should == {:message => "value1"} |
|
56 |
+ Event.last.payload[:original_event].should == { 'message' => "value1" } |
|
57 | 57 |
end |
58 | 58 |
it "should handle multiple events" do |
59 | 59 |
event1 = Event.new |
@@ -3,16 +3,16 @@ require 'spec_helper' |
||
3 | 3 |
describe Agents::TriggerAgent do |
4 | 4 |
before do |
5 | 5 |
@valid_params = { |
6 |
- :name => "my trigger agent", |
|
7 |
- :options => { |
|
8 |
- :expected_receive_period_in_days => 2, |
|
9 |
- :rules => [{ |
|
10 |
- :type => "regex", |
|
11 |
- 'value' => "a\\db", |
|
12 |
- :path => "foo.bar.baz", |
|
13 |
- }], |
|
14 |
- :message => "I saw '<foo.bar.baz>' from <name>" |
|
15 |
- } |
|
6 |
+ 'name' => "my trigger agent", |
|
7 |
+ 'options' => { |
|
8 |
+ 'expected_receive_period_in_days' => 2, |
|
9 |
+ 'rules' => [{ |
|
10 |
+ 'type' => "regex", |
|
11 |
+ 'value' => "a\\db", |
|
12 |
+ 'path' => "foo.bar.baz", |
|
13 |
+ }], |
|
14 |
+ 'message' => "I saw '<foo.bar.baz>' from <name>" |
|
15 |
+ } |
|
16 | 16 |
} |
17 | 17 |
|
18 | 18 |
@checker = Agents::TriggerAgent.new(@valid_params) |
@@ -21,8 +21,8 @@ describe Agents::TriggerAgent do |
||
21 | 21 |
|
22 | 22 |
@event = Event.new |
23 | 23 |
@event.agent = agents(:bob_rain_notifier_agent) |
24 |
- @event.payload = { :foo => { "bar" => { :baz => "a2b" }}, |
|
25 |
- :name => "Joe" } |
|
24 |
+ @event.payload = { 'foo' => { "bar" => { 'baz' => "a2b" }}, |
|
25 |
+ 'name' => "Joe" } |
|
26 | 26 |
end |
27 | 27 |
|
28 | 28 |
describe "validation" do |
@@ -31,22 +31,22 @@ describe Agents::TriggerAgent do |
||
31 | 31 |
end |
32 | 32 |
|
33 | 33 |
it "should validate presence of options" do |
34 |
- @checker.options[:message] = nil |
|
34 |
+ @checker.options['message'] = nil |
|
35 | 35 |
@checker.should_not be_valid |
36 | 36 |
end |
37 | 37 |
|
38 | 38 |
it "should validate the three fields in each rule" do |
39 |
- @checker.options[:rules] << { :path => "foo", :type => "fake", :value => "6" } |
|
39 |
+ @checker.options['rules'] << { 'path' => "foo", 'type' => "fake", 'value' => "6" } |
|
40 | 40 |
@checker.should_not be_valid |
41 |
- @checker.options[:rules].last[:type] = "field>=value" |
|
41 |
+ @checker.options['rules'].last['type'] = "field>=value" |
|
42 | 42 |
@checker.should be_valid |
43 |
- @checker.options[:rules].last.delete(:value) |
|
43 |
+ @checker.options['rules'].last.delete('value') |
|
44 | 44 |
@checker.should_not be_valid |
45 | 45 |
end |
46 | 46 |
end |
47 | 47 |
|
48 | 48 |
describe "#working?" do |
49 |
- it "checks to see if the Agent has received any events in the last :expected_receive_period_in_days days" do |
|
49 |
+ it "checks to see if the Agent has received any events in the last 'expected_receive_period_in_days' days" do |
|
50 | 50 |
@event.save! |
51 | 51 |
|
52 | 52 |
@checker.should_not be_working # no events have ever been received |
@@ -60,30 +60,30 @@ describe Agents::TriggerAgent do |
||
60 | 60 |
|
61 | 61 |
describe "#receive" do |
62 | 62 |
it "handles regex" do |
63 |
- @event.payload[:foo]["bar"][:baz] = "a222b" |
|
63 |
+ @event.payload['foo']['bar']['baz'] = "a222b" |
|
64 | 64 |
lambda { |
65 | 65 |
@checker.receive([@event]) |
66 | 66 |
}.should_not change { Event.count } |
67 | 67 |
|
68 |
- @event.payload[:foo]["bar"][:baz] = "a2b" |
|
68 |
+ @event.payload['foo']['bar']['baz'] = "a2b" |
|
69 | 69 |
lambda { |
70 | 70 |
@checker.receive([@event]) |
71 | 71 |
}.should change { Event.count }.by(1) |
72 | 72 |
end |
73 | 73 |
|
74 | 74 |
it "handles negated regex" do |
75 |
- @event.payload[:foo]["bar"][:baz] = "a2b" |
|
76 |
- @checker.options[:rules][0] = { |
|
77 |
- :type => "!regex", |
|
78 |
- :value => "a\\db", |
|
79 |
- :path => "foo.bar.baz", |
|
80 |
- } |
|
75 |
+ @event.payload['foo']['bar']['baz'] = "a2b" |
|
76 |
+ @checker.options['rules'][0] = { |
|
77 |
+ 'type' => "!regex", |
|
78 |
+ 'value' => "a\\db", |
|
79 |
+ 'path' => "foo.bar.baz", |
|
80 |
+ } |
|
81 | 81 |
|
82 | 82 |
lambda { |
83 | 83 |
@checker.receive([@event]) |
84 | 84 |
}.should_not change { Event.count } |
85 | 85 |
|
86 |
- @event.payload[:foo]["bar"][:baz] = "a22b" |
|
86 |
+ @event.payload['foo']['bar']['baz'] = "a22b" |
|
87 | 87 |
lambda { |
88 | 88 |
@checker.receive([@event]) |
89 | 89 |
}.should change { Event.count }.by(1) |
@@ -91,49 +91,49 @@ describe Agents::TriggerAgent do |
||
91 | 91 |
|
92 | 92 |
it "puts can extract values into the message based on paths" do |
93 | 93 |
@checker.receive([@event]) |
94 |
- Event.last.payload[:message].should == "I saw 'a2b' from Joe" |
|
94 |
+ Event.last.payload['message'].should == "I saw 'a2b' from Joe" |
|
95 | 95 |
end |
96 | 96 |
|
97 | 97 |
it "handles numerical comparisons" do |
98 |
- @event.payload[:foo]["bar"][:baz] = "5" |
|
99 |
- @checker.options[:rules].first[:value] = 6 |
|
100 |
- @checker.options[:rules].first[:type] = "field<value" |
|
98 |
+ @event.payload['foo']['bar']['baz'] = "5" |
|
99 |
+ @checker.options['rules'].first['value'] = 6 |
|
100 |
+ @checker.options['rules'].first['type'] = "field<value" |
|
101 | 101 |
|
102 | 102 |
lambda { |
103 | 103 |
@checker.receive([@event]) |
104 | 104 |
}.should change { Event.count }.by(1) |
105 | 105 |
|
106 |
- @checker.options[:rules].first[:value] = 3 |
|
106 |
+ @checker.options['rules'].first['value'] = 3 |
|
107 | 107 |
lambda { |
108 | 108 |
@checker.receive([@event]) |
109 | 109 |
}.should_not change { Event.count } |
110 | 110 |
end |
111 | 111 |
|
112 | 112 |
it "handles exact comparisons" do |
113 |
- @event.payload[:foo]["bar"][:baz] = "hello world" |
|
114 |
- @checker.options[:rules].first[:type] = "field==value" |
|
113 |
+ @event.payload['foo']['bar']['baz'] = "hello world" |
|
114 |
+ @checker.options['rules'].first['type'] = "field==value" |
|
115 | 115 |
|
116 |
- @checker.options[:rules].first[:value] = "hello there" |
|
116 |
+ @checker.options['rules'].first['value'] = "hello there" |
|
117 | 117 |
lambda { |
118 | 118 |
@checker.receive([@event]) |
119 | 119 |
}.should_not change { Event.count } |
120 | 120 |
|
121 |
- @checker.options[:rules].first[:value] = "hello world" |
|
121 |
+ @checker.options['rules'].first['value'] = "hello world" |
|
122 | 122 |
lambda { |
123 | 123 |
@checker.receive([@event]) |
124 | 124 |
}.should change { Event.count }.by(1) |
125 | 125 |
end |
126 | 126 |
|
127 | 127 |
it "handles negated comparisons" do |
128 |
- @event.payload[:foo]["bar"][:baz] = "hello world" |
|
129 |
- @checker.options[:rules].first[:type] = "field!=value" |
|
130 |
- @checker.options[:rules].first[:value] = "hello world" |
|
128 |
+ @event.payload['foo']['bar']['baz'] = "hello world" |
|
129 |
+ @checker.options['rules'].first['type'] = "field!=value" |
|
130 |
+ @checker.options['rules'].first['value'] = "hello world" |
|
131 | 131 |
|
132 | 132 |
lambda { |
133 | 133 |
@checker.receive([@event]) |
134 | 134 |
}.should_not change { Event.count } |
135 | 135 |
|
136 |
- @checker.options[:rules].first[:value] = "hello there" |
|
136 |
+ @checker.options['rules'].first['value'] = "hello there" |
|
137 | 137 |
|
138 | 138 |
lambda { |
139 | 139 |
@checker.receive([@event]) |
@@ -141,20 +141,20 @@ describe Agents::TriggerAgent do |
||
141 | 141 |
end |
142 | 142 |
|
143 | 143 |
it "does fine without dots in the path" do |
144 |
- @event.payload = { :hello => "world" } |
|
145 |
- @checker.options[:rules].first[:type] = "field==value" |
|
146 |
- @checker.options[:rules].first[:path] = "hello" |
|
147 |
- @checker.options[:rules].first[:value] = "world" |
|
144 |
+ @event.payload = { 'hello' => "world" } |
|
145 |
+ @checker.options['rules'].first['type'] = "field==value" |
|
146 |
+ @checker.options['rules'].first['path'] = "hello" |
|
147 |
+ @checker.options['rules'].first['value'] = "world" |
|
148 | 148 |
lambda { |
149 | 149 |
@checker.receive([@event]) |
150 | 150 |
}.should change { Event.count }.by(1) |
151 | 151 |
|
152 |
- @checker.options[:rules].first[:path] = "foo" |
|
152 |
+ @checker.options['rules'].first['path'] = "foo" |
|
153 | 153 |
lambda { |
154 | 154 |
@checker.receive([@event]) |
155 | 155 |
}.should_not change { Event.count } |
156 | 156 |
|
157 |
- @checker.options[:rules].first[:value] = "hi" |
|
157 |
+ @checker.options['rules'].first['value'] = "hi" |
|
158 | 158 |
lambda { |
159 | 159 |
@checker.receive([@event]) |
160 | 160 |
}.should_not change { Event.count } |
@@ -163,11 +163,11 @@ describe Agents::TriggerAgent do |
||
163 | 163 |
it "handles multiple events" do |
164 | 164 |
event2 = Event.new |
165 | 165 |
event2.agent = agents(:bob_weather_agent) |
166 |
- event2.payload = { :foo => { "bar" => { :baz => "a2b" }}} |
|
166 |
+ event2.payload = { 'foo' => { 'bar' => { 'baz' => "a2b" }}} |
|
167 | 167 |
|
168 | 168 |
event3 = Event.new |
169 | 169 |
event3.agent = agents(:bob_weather_agent) |
170 |
- event3.payload = { :foo => { "bar" => { :baz => "a222b" }}} |
|
170 |
+ event3.payload = { 'foo' => { 'bar' => { 'baz' => "a222b" }}} |
|
171 | 171 |
|
172 | 172 |
lambda { |
173 | 173 |
@checker.receive([@event, event2, event3]) |
@@ -175,19 +175,19 @@ describe Agents::TriggerAgent do |
||
175 | 175 |
end |
176 | 176 |
|
177 | 177 |
it "handles ANDing rules together" do |
178 |
- @checker.options[:rules] << { |
|
179 |
- :type => "field>=value", |
|
180 |
- :value => "4", |
|
181 |
- :path => "foo.bing" |
|
178 |
+ @checker.options['rules'] << { |
|
179 |
+ 'type' => "field>=value", |
|
180 |
+ 'value' => "4", |
|
181 |
+ 'path' => "foo.bing" |
|
182 | 182 |
} |
183 | 183 |
|
184 |
- @event.payload[:foo]["bing"] = "5" |
|
184 |
+ @event.payload['foo']["bing"] = "5" |
|
185 | 185 |
|
186 | 186 |
lambda { |
187 | 187 |
@checker.receive([@event]) |
188 | 188 |
}.should change { Event.count }.by(1) |
189 | 189 |
|
190 |
- @checker.options[:rules].last[:value] = 6 |
|
190 |
+ @checker.options['rules'].last['value'] = 6 |
|
191 | 191 |
lambda { |
192 | 192 |
@checker.receive([@event]) |
193 | 193 |
}.should_not change { Event.count } |
@@ -53,7 +53,7 @@ describe Agents::TwitterStreamAgent do |
||
53 | 53 |
@agent.memory[:filter_counts] = {:keyword1 => 2, :keyword2 => 3, :keyword3 => 4} |
54 | 54 |
@agent.save! |
55 | 55 |
@agent.process_tweet('keyword1', {:text => "something", :user => {:name => "Mr. Someone"}}) |
56 |
- @agent.reload.memory[:filter_counts].should == {:keyword1 => 3, :keyword2 => 3} |
|
56 |
+ @agent.reload.memory[:filter_counts].should == { 'keyword1' => 3, 'keyword2' => 3 } |
|
57 | 57 |
end |
58 | 58 |
end |
59 | 59 |
|
@@ -64,9 +64,9 @@ describe Agents::TwitterStreamAgent do |
||
64 | 64 |
}.should change { @agent.events.count }.by(1) |
65 | 65 |
|
66 | 66 |
@agent.events.last.payload.should == { |
67 |
- :filter => 'keyword1', |
|
68 |
- :text => "something", |
|
69 |
- :user => {:name => "Mr. Someone"} |
|
67 |
+ 'filter' => 'keyword1', |
|
68 |
+ 'text' => "something", |
|
69 |
+ 'user' => { 'name' => "Mr. Someone" } |
|
70 | 70 |
} |
71 | 71 |
end |
72 | 72 |
|
@@ -79,9 +79,9 @@ describe Agents::TwitterStreamAgent do |
||
79 | 79 |
}.should change { @agent.events.count }.by(1) |
80 | 80 |
|
81 | 81 |
@agent.events.last.payload.should == { |
82 |
- :filter => 'keyword1-1', |
|
83 |
- :text => "something", |
|
84 |
- :user => {:name => "Mr. Someone"} |
|
82 |
+ 'filter' => 'keyword1-1', |
|
83 |
+ 'text' => "something", |
|
84 |
+ 'user' => { 'name' => "Mr. Someone" } |
|
85 | 85 |
} |
86 | 86 |
end |
87 | 87 |
end |
@@ -0,0 +1,31 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe Agents::WebhookAgent do |
|
4 |
+ let(:agent) do |
|
5 |
+ _agent = Agents::WebhookAgent.new(:name => 'webhook', |
|
6 |
+ :options => { 'secret' => 'foobar', 'payload_path' => 'payload' }) |
|
7 |
+ _agent.user = users(:bob) |
|
8 |
+ _agent.save! |
|
9 |
+ _agent |
|
10 |
+ end |
|
11 |
+ let(:payload) { {'some' => 'info'} } |
|
12 |
+ |
|
13 |
+ describe 'receive_webhook' do |
|
14 |
+ it 'should create event if secret matches' do |
|
15 |
+ out = nil |
|
16 |
+ lambda { |
|
17 |
+ out = agent.receive_webhook('secret' => 'foobar', 'payload' => payload) |
|
18 |
+ }.should change { Event.count }.by(1) |
|
19 |
+ out.should eq(['Event Created', 201]) |
|
20 |
+ Event.last.payload.should eq(payload) |
|
21 |
+ end |
|
22 |
+ |
|
23 |
+ it 'should not create event if secrets dont match' do |
|
24 |
+ out = nil |
|
25 |
+ lambda { |
|
26 |
+ out = agent.receive_webhook('secret' => 'bazbat', 'payload' => payload) |
|
27 |
+ }.should change { Event.count }.by(0) |
|
28 |
+ out.should eq(['Not Authorized', 401]) |
|
29 |
+ end |
|
30 |
+ end |
|
31 |
+end |
@@ -44,18 +44,22 @@ describe Agents::WebsiteAgent do |
||
44 | 44 |
|
45 | 45 |
describe '#working?' do |
46 | 46 |
it 'checks if events have been received within the expected receive period' do |
47 |
+ stubbed_time = Time.now |
|
48 |
+ stub(Time).now { stubbed_time } |
|
49 |
+ |
|
47 | 50 |
@checker.should_not be_working # No events created |
48 | 51 |
@checker.check |
49 | 52 |
@checker.reload.should be_working # Just created events |
50 | 53 |
|
51 | 54 |
@checker.error "oh no!" |
52 |
- @checker.reload.should_not be_working # The most recent log is an error |
|
55 |
+ @checker.reload.should_not be_working # There is a recent error |
|
53 | 56 |
|
54 |
- @checker.log "ok now" |
|
55 |
- @checker.reload.should be_working # The most recent log is no longer an error |
|
57 |
+ stubbed_time = 20.minutes.from_now |
|
58 |
+ @checker.events.delete_all |
|
59 |
+ @checker.check |
|
60 |
+ @checker.reload.should be_working # There is a newer event now |
|
56 | 61 |
|
57 |
- two_days_from_now = 2.days.from_now |
|
58 |
- stub(Time).now { two_days_from_now } |
|
62 |
+ stubbed_time = 2.days.from_now |
|
59 | 63 |
@checker.reload.should_not be_working # Two days have passed without a new event having been created |
60 | 64 |
end |
61 | 65 |
end |